thinkpool-pair 0.7.365 → 0.7.367

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/LICENSE CHANGED
@@ -1,11 +1,11 @@
1
- Thinkpool Proprietary Software Notice
1
+ thinkpool Proprietary Software Notice
2
2
 
3
3
  Copyright © 2026 Thinkpool, UAB. All rights reserved.
4
4
 
5
5
  This software is proprietary.
6
6
 
7
7
  A person who lawfully obtains thinkpool-pair may install and run an
8
- unmodified copy solely to access and use the Thinkpool service in accordance
8
+ unmodified copy solely to access and use the thinkpool service in accordance
9
9
  with the Terms of Service at https://thinkpool.io/terms.
10
10
 
11
11
  Except for that limited permission, no licence is granted to copy, modify,
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # `thinkpool-pair`
2
2
 
3
3
  `thinkpool-pair` connects coding agents running on a machine you control to a
4
- Thinkpool Code room. Both people in the room can watch and steer the visible
4
+ thinkpool Code room. Both people in the room can watch and steer the visible
5
5
  agent lanes from a browser or phone.
6
6
 
7
- The bridge makes outbound connections to Thinkpool’s Supabase project. It does
7
+ The bridge makes outbound connections to thinkpool’s Supabase project. It does
8
8
  not require an inbound port, tunnel, or public IP.
9
9
 
10
10
  ## Requirements
11
11
 
12
12
  - Node.js 18 or newer
13
- - A Thinkpool account and Code session
13
+ - A thinkpool account and Code session
14
14
  - At least one supported runtime available on the host:
15
15
  - Claude
16
16
  - Codex
@@ -128,7 +128,7 @@ have access to the bound project and the host permissions granted to that lane.
128
128
  |---|---|
129
129
  | Claude | Uses the host’s normal Anthropic login by default. A registered Anthropic-compatible provider can be selected for a lane. |
130
130
  | Codex | Uses the host’s Codex/OpenAI login. Custom bridge providers are not wired to Codex lanes. |
131
- | Hermes | Uses an isolated Thinkpool ACP profile. Set it up explicitly before the first Hermes lane. |
131
+ | Hermes | Uses an isolated thinkpool ACP profile. Set it up explicitly before the first Hermes lane. |
132
132
 
133
133
  The launcher’s **Settings → Provider** flow configures the host default for
134
134
  Claude-compatible lanes. The equivalent CLI commands are:
@@ -188,10 +188,10 @@ The bridge and web app deliberately split responsibility:
188
188
 
189
189
  - Agent processes, repository access, provider credentials, and raw terminal
190
190
  bytes remain on the bridge machine.
191
- - Thinkpool relays room events and terminal output to authorized room members.
192
- - Thinkpool stores the cleaned reader transcript and room events so members can
191
+ - thinkpool relays room events and terminal output to authorized room members.
192
+ - thinkpool stores the cleaned reader transcript and room events so members can
193
193
  reopen the room.
194
- - Thinkpool does not store a copy of the repository or a second copy of the raw
194
+ - thinkpool does not store a copy of the repository or a second copy of the raw
195
195
  PTY byte stream.
196
196
  - Voice dictation is an application API path and uses the configured
197
197
  transcription service; it is not a local-only bridge operation.
@@ -229,7 +229,7 @@ the preview server’s loopback origin for page requests.
229
229
  |---|---|
230
230
  | `TP_NAME` | Label this bridge in the room |
231
231
  | `TP_PAIR_ROOT` | Override the local state directory |
232
- | `TP_SUPABASE_URL` / `TP_SUPABASE_ANON` | Override the built-in Thinkpool project endpoint/public key |
232
+ | `TP_SUPABASE_URL` / `TP_SUPABASE_ANON` | Override the built-in thinkpool project endpoint/public key |
233
233
  | `TP_ANTHROPIC_BASE_URL` / `TP_ANTHROPIC_AUTH_TOKEN` / `TP_ANTHROPIC_MODEL` | Override the Claude-compatible provider without writing provider config |
234
234
  | `TP_CONTEXT_MAX` | Override the context-window size used by the room meter |
235
235
  | `TP_BROWSER_PATH` | Point preview capture at a non-standard Chrome/Chromium binary |
@@ -322,6 +322,6 @@ authority. Do not use it as a local verification command.
322
322
  ## Licence
323
323
 
324
324
  `thinkpool-pair` is proprietary software. A lawful recipient may install and
325
- run an unmodified copy to use Thinkpool. Redistribution, modification, resale,
325
+ run an unmodified copy to use thinkpool. Redistribution, modification, resale,
326
326
  and other uses require prior written permission. See [LICENSE](LICENSE) and
327
- the [Thinkpool Terms of Service](https://thinkpool.io/terms).
327
+ the [thinkpool Terms of Service](https://thinkpool.io/terms).
@@ -1 +1 @@
1
- export function queueAbortBarrier(r,e=()=>r?.session?.abort?.()){if(!r)return Promise.resolve();const o=r._abortBarrier||Promise.resolve();let t;return t=Promise.resolve(o).catch(()=>{}).then(()=>e()).catch(()=>{}).finally(()=>{r._abortBarrier===t&&(r._abortBarrier=null)}),r._abortBarrier=t,t}export async function waitForAbortBarrier(r){const e=r?._abortBarrier;e&&await e}
1
+ const r=r=>{const e=Number(r);return Number.isSafeInteger(e)&&e>0?e:0},e=(r,e)=>{for(r._abortCommandCids||(r._abortCommandCids=new Set),r._abortCommandCids.add(e);r._abortCommandCids.size>128;)r._abortCommandCids.delete(r._abortCommandCids.values().next().value)};export function admitAbortRequest(t,n={}){const o="string"==typeof n.cid?n.cid.trim():"",u=r(n.turnRev),i="string"==typeof n.turnCid?n.turnCid.trim():"",a=r(t?._turnRev);return t?.session?o?t._abortCommandCids?.has(o)?{action:"duplicate",code:"duplicate",currentTurnRev:a}:u?u<a?{action:"reject",code:"stale_turn",currentTurnRev:a}:u>a+1?{action:"reject",code:"future_turn",currentTurnRev:a}:u===a+1?!0===t._busyAnn?{action:"reject",code:"future_turn",currentTurnRev:a}:i?(e(t,o),t._pendingAbortRequest={cid:o,turnRev:u,turnCid:i},{action:"defer",code:"awaiting_turn",currentTurnRev:a,targetTurnRev:u}):{action:"reject",code:"missing_turn_id",currentTurnRev:a}:!0!==t._busyAnn?{action:"reject",code:"turn_already_settled",currentTurnRev:a}:(e(t,o),{action:"abort",code:"accepted",currentTurnRev:a,targetTurnRev:u}):{action:"reject",code:"missing_turn_revision",currentTurnRev:a}:{action:"reject",code:"missing_abort_id",currentTurnRev:a}:{action:"reject",code:"session_unavailable",currentTurnRev:a}}export function takePendingAbortRequest(e,{turnRev:t,turnCid:n}={}){const o=e?._pendingAbortRequest;if(!o)return null;const u=r(t);return u<o.turnRev?null:(e._pendingAbortRequest=null,u===o.turnRev&&n&&n===o.turnCid?o:null)}export function queueAbortBarrier(r,e=()=>r?.session?.abort?.()){if(!r)return Promise.resolve();const t=r._abortBarrier||Promise.resolve();let n;return n=Promise.resolve(t).catch(()=>{}).then(()=>e()).catch(()=>{}).finally(()=>{r._abortBarrier===n&&(r._abortBarrier=null)}),r._abortBarrier=n,n}export async function waitForAbortBarrier(r){const e=r?._abortBarrier;e&&await e}
package/account.mjs CHANGED
@@ -1 +1 @@
1
- import{spawn as e}from"node:child_process";import{fileURLToPath as r}from"node:url";import{randomUUID as t}from"node:crypto";import o from"node:path";import n from"node:fs";import{createClient as s}from"@supabase/supabase-js";import i from"node:os";import{saveAuth as a,loadAuth as c,markAuthReconnectRequired as d,loadDirs as l,bindDir as p,loadServed as u,rememberServed as m,loadDefaultDir as f,saveDefaultDir as h,claudeRecentProjectDir as y}from"./auth-store.mjs";import{isSafeToRestart as v}from"./update-gate.mjs";import{makeThrottledTrack as g,presenceRecoveryEligible as w,presenceSelfEchoObservation as _,recoveryBackoffMs as b,reducePresenceChannelEvidence as k,subscribeWithoutStartupDeadlock as $}from"./presence.mjs";import{publicKeyB64 as R,announceProviders as S,listProviders as T,addProvider as I,addProviderModel as O,removeProvider as x,unseal as C}from"./providers.mjs";import{supervisorServes as A,supervisorRoomsToStop as P}from"./serve-consent.mjs";import{resolveServeDir as N}from"./serve-dir.mjs";import{pairKeyFor as E,pairTopic as q,CROSSROOM_BUS as M}from"./cross-terminal.mjs";import{installedAgentCommands as U}from"./agent-detect.mjs";import{agentVisibilityStates as D,setAgentPickerVisibility as j}from"./agent-visibility.mjs";import{hostMemoryAdmission as F}from"./host-memory.mjs";import{createPairBusBroker as L,mergePairRoomRoster as H,pairBusStatusFromRealtime as B,PAIR_BUS_STATUS as G}from"./pair-bus.mjs";import{clearSupervisorReady as z,supervisorPresenceEchoed as K,writeSupervisorReady as W}from"./supervisor-ready.mjs";import{PAIR_CLI as J,pairCli as V}from"./command-guidance.mjs";import{syncRealtimeAuth as X}from"./design-edit.mjs";const Y=(()=>{try{return JSON.parse(n.readFileSync(new URL("./package.json",import.meta.url),"utf8")).version}catch{return null}})(),Z=r(new URL("./bridge.mjs",import.meta.url));export function createStartupAnchor({set:e=setInterval,clear:r=clearInterval}={}){const t=e(()=>{},6e4);let o=!1;return()=>{o||(o=!0,r(t))}}const Q=o.join(i.homedir(),".thinkpool-pair","account.lock");function ee(e){if(!e||e===process.pid)return!1;try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}export function acquireSingleton(e=Q){try{n.mkdirSync(o.dirname(e),{recursive:!0})}catch{return!1}for(let r=0;r<3;r++)try{const r=n.openSync(e,"wx");return n.writeSync(r,String(process.pid)),n.closeSync(r),!0}catch(r){if("EEXIST"!==r.code)return!1;let t=0;try{t=parseInt(n.readFileSync(e,"utf8").trim(),10)||0}catch{return!1}if(ee(t))return!1;try{n.rmSync(e,{force:!0})}catch{return!1}}return!1}export function releaseSingleton(e=Q){try{(parseInt(n.readFileSync(e,"utf8").trim(),10)||0)===process.pid&&n.rmSync(e,{force:!0})}catch{}}export function acquireMachineLock(e=Q){return acquireSingleton(e)}export function releaseMachineLock(e=Q){return releaseSingleton(e)}export function shouldHoldMachineLock(e=process.env){return"1"!==e.THINKPOOL_PAIR_ACCOUNT_CHILD}export async function runLogin(e,r,t){const o=s(e,r,{auth:{persistSession:!1}}),{data:n,error:i}=await o.rpc("start_device_code"),c=Array.isArray(n)?n[0]:n;!i&&c?.device_code||(process.stderr.write(`\n ✗ couldn't start login: ${i?.message||"no response"}\n`),process.exit(1));const{device_code:d,user_code:l}=c,p=`${t}/code/link?c=${encodeURIComponent(l)}`;process.stderr.write(`\n ◆ Link this device to your ThinkPool account.\n Open (signed in) and approve:\n\n ${p}\n\n code: ${l}\n\n Waiting…\n`);const u=Date.now(),m=async()=>{Date.now()-u>3e5&&(process.stderr.write(`\n ◇ link timed out — run \`${V("login")}\` again.\n`),process.exit(1));try{const{data:e}=await o.rpc("claim_device_code",{p_device_code:d});"approved"===e?.status&&e.session?.refresh_token&&(a(e.session),process.stderr.write(`\n ◆ linked as ${e.session.email||"your account"}. You can close the browser tab.\n Now run: ${J}\n`),process.exit(0)),"expired"!==e?.status&&"not_found"!==e?.status||(process.stderr.write(`\n ◇ link expired — run \`${V("login")}\` again.\n`),process.exit(1))}catch{}setTimeout(m,3e3)};m(),await new Promise(()=>{})}const re=e=>new Promise(r=>{const t=setTimeout(r,e);t.unref?.()});export function withTimeout(e,r,t="op"){return new Promise((o,n)=>{const s=setTimeout(()=>n(new Error(`${t} timed out after ${r}ms`)),r);s.unref?.(),Promise.resolve(e).then(e=>{clearTimeout(s),o(e)},e=>{clearTimeout(s),n(e)})})}export function savedTokenStillValid(e,r=Math.floor(Date.now()/1e3),t=300){if(!e?.access_token)return!1;const o=e.expires_at||function(e){try{const r=String(e).split(".")[1],t=Buffer.from(r.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"),o=JSON.parse(t)?.exp;return"number"==typeof o?o:0}catch{return 0}}(e.access_token);return!!o&&o-r>t}const te=o.join(i.homedir(),".thinkpool-pair","refresh.lock");export function acquireRefreshLock(e=te,r=3e4){try{n.mkdirSync(o.dirname(e),{recursive:!0})}catch{}for(let t=0;t<2;t++)try{const r=n.openSync(e,"wx");return n.writeSync(r,JSON.stringify({pid:process.pid,at:Date.now()})),n.closeSync(r),!0}catch(t){if("EEXIST"!==t.code)return!0;let o=1/0;try{o=Date.now()-(JSON.parse(n.readFileSync(e,"utf8"))?.at||0)}catch{o=1/0}if(o<r)return!1;try{n.rmSync(e,{force:!0})}catch{return!0}}return!1}export function releaseRefreshLock(e=te){try{n.rmSync(e,{force:!0})}catch{}}async function oe(e,r){if(!r?.access_token||!r?.refresh_token)return null;try{const{data:t,error:o}=await e.auth.setSession({access_token:r.access_token,refresh_token:r.refresh_token});return o||!t?.session?null:t.session}catch{return null}}export async function refreshAccountSession(e,r,t={}){const{load:o=c,save:n=a,acquire:s=acquireRefreshLock,release:i=releaseRefreshLock,wait:d=re}=t,l=async r=>{const t=o();if(t?.refresh_token&&r&&t.refresh_token!==r){const r=await oe(e,t);if(r)return{session:r,adopted:!0}}return null};let p=await l(r);if(p)return p;const u=o()?.refresh_token||r;if(!u)return null;let m=s();if(!m){for(let e=0;e<20;e++){if(await d(250),p=await l(u),p)return p;if(s()){m=!0;break}}if(!m)return null}try{if(p=await l(u),p)return p;const r=o()?.refresh_token||u,{data:t,error:s}=await e.auth.refreshSession({refresh_token:r});return s||!t?.session?{session:null,error:s||null}:(n(t.session),{session:t.session,adopted:!1})}finally{m&&i()}}export const ACCOUNT_RECONNECT_ERROR_CODES=new Set(["refresh_token_not_found","refresh_token_already_used","session_not_found"]);export function accountRefreshNeedsReconnect(e){return ACCOUNT_RECONNECT_ERROR_CODES.has(String(e?.code||""))}export async function authedClient(e,r){const t=c();if(!t?.refresh_token)return null;if(t.reconnect_required)return{reconnectRequired:!0};const o=s(e,r,{auth:{persistSession:!1,autoRefreshToken:!1}});if(savedTokenStillValid(t)){const e=await oe(o,t);if(e)return{sb:o,session:e}}const n=await refreshAccountSession(o,t.refresh_token);return n?.session?{sb:o,session:n.session}:accountRefreshNeedsReconnect(n?.error)?(d(n.error.code),{reconnectRequired:!0}):null}export async function authedClientWithRetry(e,r,t={}){const{attempt:o=authedClient,load:n=c,wait:s=re,onRetry:i=null,maxAttempts:a=1/0,baseMs:d=15e3,capMs:l=3e5}=t;let p=d;for(let t=1;t<=a;t++){const c=await o(e,r);if(c?.reconnectRequired)return c;if(c)return c;if(!n()?.refresh_token)return null;if(t>=a)break;i?.(p,t),await s(p),p=Math.min(2*p,l)}return null}export function claimLoopWedged({lastClaimOkAt:e,now:r,quietMsThreshold:t=4e4}){return r-e>=t}export function scheduleTokenRefresh({sb:e,session:r,onRefreshed:t,onReconnectRequired:o,skewMs:n=9e4,forceMs:s=0,retryMs:i=3e4,refresh:a=refreshAccountSession}){let c=null,d=r,l=!1,p=!1;const u=e=>{l||(clearTimeout(c),c=setTimeout(f,Math.max(1e3,e)),c?.unref&&c.unref())},m=()=>{if(s)return s;const e=d?.expires_at?1e3*d.expires_at:Date.now()+36e5;return Math.max(3e4,e-Date.now()-n)};async function f(){if(!l)try{const r=await a(e,d.refresh_token);if(!r?.session){if(accountRefreshNeedsReconnect(r?.error)&&!p){p=!0;try{await(o?.(r.error))}catch{}}throw r?.error||new Error("no session")}d=r.session;try{e.realtime?.setAuth?.(d.access_token)}catch{}p=!1;try{await(t?.(d))}catch{}u(m())}catch{u(i)}}return u(m()),{cancel:()=>{l=!0,clearTimeout(c)},_tick:f}}export function runBind(e,r){const t=(e||"").toUpperCase().trim();t||(console.error(`usage: ${V("bind","<ROOM>","<directory>")}`),process.exit(1));const s=o.resolve(r||".");n.existsSync(s)&&n.statSync(s).isDirectory()||(console.error(`✗ not a directory: ${s}`),process.exit(1)),p(t,s),console.error(`✓ bound ${t} → ${s}\n Run \`${J}\` (or restart it) to serve this session.`),process.exit(0)}export async function runAccount(r,s){acquireSingleton()||(console.error(`\n ◇ An account bridge is already running on this machine\n (~/.thinkpool-pair/account.lock). Not starting a second — two would race\n your saved login. Stop the other one first, or share a single room with\n \`${V("<ROOM>")}\`.\n`),process.exit(0)),z();const c=createStartupAnchor(),J=await authedClientWithRetry(r,s,{onRetry:e=>process.stderr.write(`\n ◇ Couldn't refresh your account session yet (offline, or another bridge is mid-rotation) — retrying in ${Math.round(e/1e3)}s. Saved login left in place.\n`)});J||(releaseSingleton(),console.error(`\n ◇ Not linked to a ThinkPool account on this machine.\n Link it: ${V("login")}\n`),process.exit(0)),J.reconnectRequired&&(c(),releaseSingleton(),console.error("\n ◇ This bridge lost its Thinkpool account link.\n Reconnect it: npx thinkpool-pair@latest login\n"),process.exit(0));const{sb:Q,session:ee}=J,re=ee.user?.email||"your account";let te=ee.access_token||null;const oe=process.cwd();let ne=!1;try{const e=await import("./service.mjs");ne=e.serviceActive(null)}catch{}const se=()=>"1"===process.env.THINKPOOL_PAIR_AUTOUPDATE||ne,ie="1"===process.env.THINKPOOL_PAIR_AUTOUPDATE?" (auto-updating background service — survives reboot, tracks @latest)\n":ne?" (background service — survives reboot, restarts on crash, pinned to this version)\n":` (run hands-off + auto-updating: ${V("install-service")})\n`,ae=f()||y()||oe;process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${re}\n New sessions auto-serve from ${ae}${f()?" (your default)":y()?" (your most-recent Claude Code project)":""}\n (change it: ${V("set-default-dir","<dir>")} · one room: ${V("bind","<code>","<dir>")})\n${ie}`);const ce=new Map,de=new Map,le=new Map,pe=new Map,ue=new Set,me=new Map,fe=new Map,he=new Map,ye=new Map,ve=new Map;let ge=!1,we=null,_e=!1;const be=new Map,ke=new Set,$e=new Map,Re=i.hostname().replace(/\.local$/,""),Se=(t?.()||"").slice(0,8);let Te=null;console.log(`◇ supervisor boot sup=${Se} host=${Re} pid=${process.pid} npx=${Y||"dev"}`),await X(Q,te,s)||process.stderr.write("\n ⚠ realtime auth could not be initialized; private bridge control is unavailable.\n");const Ie=Q.channel(`tpacct:${ee.user.id}`,{config:{presence:{key:Re},broadcast:{self:!1}}}),Oe=Q.channel(`tpacct-control:${ee.user.id}`,{config:{private:!0,broadcast:{self:!1}}}),xe=t();let Ce=!1,Ae=()=>{};Oe.on("broadcast",{event:"bind-room"},({payload:e})=>{const r=String(e?.code||"").toUpperCase().trim();let t=String(e?.dir||"").trim();t.startsWith("~")&&(t=o.join(i.homedir(),t.slice(1))),r&&t&&(n.existsSync(t)?(p(r,t),e?.setDefault&&(h(t),process.stderr.write(`\n ◆ default project dir set → ${t}\n`)),process.stderr.write(`\n ◆ ${r} bound → ${t} (from the web). Serving…\n`),Ae()):process.stderr.write(`\n ◇ bind ${r}: directory not found (${t}) — ignored.\n`))}),Oe.on("broadcast",{event:"serve-room"},({payload:e})=>{const r=String(e?.code||"").toUpperCase().trim();/^[A-Z0-9]{4,12}$/.test(r)&&Ae()}),Oe.on("broadcast",{event:"shutdown"},async()=>{if(process.stderr.write("\n ◇ disconnect requested from the dashboard — stopping bridge.\n"),setTimeout(()=>{try{releaseSingleton()}catch{}process.exit(0)},2500),se())try{(await import("./service.mjs")).uninstallService(null)}catch{}process.kill(process.pid,"SIGTERM")});let Pe=!1;Oe.on("broadcast",{event:"restart"},async({payload:e}={})=>{const r=e?.nonce,t=async(e,t)=>{try{await Oe.send({type:"broadcast",event:"restart-status",payload:{nonce:r,ok:e,error:t}})}catch{}};if(Pe||_e)await t(!1,"a bridge update is already being prepared");else{Pe=!0;try{const r=await import("./service.mjs"),o=r.serviceActive(null),n=o?r.stageServiceUpdate(null):null;if(o&&!n)return void await t(!1,"the published runtime could not be staged; the existing bridge was left running");we=n||("string"==typeof e?.version?e.version:Y)||"restart",ge=!0,await t(!0)}catch(e){return void await t(!1,`restart preparation failed: ${e?.message||e}`)}finally{Pe=!1}gr()}});let Ne="init",Ee="connected";const qe=g(Ie,{minMs:5e3,onStatus:e=>{Ne=e}}),Me=R(),Ue=()=>qe({name:Re,bridgeId:xe,version:Y,rooms:[...ce.keys()],readyRooms:[...ue],refused:[...$e].map(([e,r])=>({code:e,reason:r})),service:se(),providerPubKey:Me,providers:S(),agents:D(U()),accountAuthState:Ee,ts:Date.now()}),De=async e=>{try{await withTimeout(Q.rpc("report_bridge_auth_state",{p_bridge_id:xe,p_state:e}),5e3,"report_bridge_auth_state")}catch{}},je=(e,r)=>{try{Oe.send({type:"broadcast",event:e,payload:r})}catch{}};Oe.on("broadcast",{event:"provider-add"},async({payload:e})=>{const r=e?.nonce;let t;try{t=C(e?.sealed)}catch{return je("provider-add-res",{nonce:r,ok:!1,error:"could not decrypt the sealed payload"})}const o=I(t);o.ok&&(Ue(),process.stderr.write(`\n ◆ provider added (${String(t?.name||"").slice(0,40)}) — re-announced.\n`)),je("provider-add-res",{nonce:r,ok:o.ok,error:o.ok?void 0:o.error,id:o.ok?o.id:void 0})}),Oe.on("broadcast",{event:"provider-add-model"},async({payload:e})=>{const r=e?.nonce,t=O({id:e?.id,model:e?.model,name:e?.name});t.ok&&(Ue(),process.stderr.write(`\n ◆ model added to an existing provider (${String(e?.model||"").slice(0,40)}) — re-announced.\n`)),je("provider-add-model-res",{nonce:r,ok:t.ok,error:t.ok?void 0:t.error,id:t.ok?t.id:void 0})}),Oe.on("broadcast",{event:"provider-remove"},async({payload:e})=>{const r=e?.nonce,t=x(e?.id);t.ok&&(Ue(),process.stderr.write("\n ◆ provider removed — re-announced.\n")),je("provider-remove-res",{nonce:r,ok:t.ok,error:t.ok?void 0:t.error})}),Oe.on("broadcast",{event:"providers-list-req"},async({payload:e})=>{const r=e?.nonce;je("providers-list-res",{nonce:r,ok:!0,providers:T()})}),Oe.on("broadcast",{event:"agent-visibility-set"},async({payload:e})=>{const r=e?.nonce,t=U(),o=j(e?.cmd,!0===e?.enabled,{installed:t});if(o.ok){Ue();for(const e of ce.values())try{e.send({t:"agent-visibility-changed"})}catch{}process.stderr.write(`\n ◆ terminal picker agents updated — ${o.agents.filter(e=>e.enabled).map(e=>e.cmd).join(", ")} visible.\n`)}je("agent-visibility-res",{nonce:r,ok:o.ok,error:o.ok?void 0:o.error,agents:o.ok?o.agents:void 0})});let Fe=k(),Le=!1,He=!1;const Be=()=>{if(He||!Le)return;let e=!1;try{e=K(Ie.presenceState?.(),Re,xe,Y)}catch{e=!1}e&&(He=W({version:Y,bridgeId:xe}),He||process.stderr.write("\n ◇ Could not record local supervisor readiness; the bridge remains running but installers will not claim it was confirmed.\n"))};Ie.on("presence",{event:"sync"},()=>{Fe=k(Fe,"sync"),Be()});const Ge=await $(Ie,e=>{if("SUBSCRIBED"===e)Fe=k(Fe,e),Ue();else if("CLOSED"===e||"CHANNEL_ERROR"===e||"TIMED_OUT"===e){Fe=k(Fe,e);try{Q.realtime?.connect?.()}catch{}}});Ge.connected||process.stderr.write(`\n ◇ account realtime not ready at startup (${Ge.status}) — staying alive and recovering in process.\n`);const ze=await $(Oe,e=>{"CLOSED"!==e&&"CHANNEL_ERROR"!==e&&"TIMED_OUT"!==e||process.stderr.write(`\n ⚠ private account control ${e}; host-changing dashboard actions are unavailable until Realtime reconnects.\n`)});ze.connected||process.stderr.write(`\n ◇ private account control not ready at startup (${ze.status}) — presence remains available.\n`);const Ke=scheduleTokenRefresh({sb:Q,session:ee,onRefreshed:async e=>{Ee="connected",a(e),Ue(),await De("connected"),te=e.access_token||te;for(const e of ce.values())try{e.send({t:"token",accessToken:te})}catch{}},onReconnectRequired:async e=>{Ee="reconnect-required",d(e?.code||"refresh-token-invalid"),Ue(),await De("reconnect-required"),process.stderr.write("\n ◇ This bridge lost its Thinkpool account link.\n Reconnect it: npx thinkpool-pair@latest login\n")},forceMs:parseInt(process.env.TP_TOKEN_REFRESH_FORCE_MS,10)||0});let We=!1,Je=0,Ve=null,Xe=Date.now(),Ye=0,Ze=0;const Qe=async()=>{try{const{data:e}=await withTimeout(Q.rpc("claim_bridge",{p_bridge_id:xe,p_hostname:Re}),1e4,"claim_bridge");Xe=Date.now();const r=Array.isArray(e)?e[0]:e,t=!!r?.held,o=r?.holder_host||"?";return t&&!We?process.stderr.write("\n ◆ holding this account — serving your sessions (single active bridge).\n"):!t&&We?process.stderr.write(`\n ◇ another device (${o}) took over this account — standing by.\n`):t||Ve===o||process.stderr.write(`\n ◇ another device (${o}) is serving this account — standing by.\n (this Mac takes over automatically if that device goes offline.)\n`),We=t,Je=t&&Number.isSafeInteger(Number(r?.fencing))&&Number(r.fencing)>0?Number(r.fencing):0,Ve=t?null:o,Te!==t&&(console.log(`claim_tick sup=${Se} held=${t} holder=${t?Re:o}`),Te=t),t}catch{return We}},er=L({channels:ye,randomId:t}),rr=e=>er.resolve(e),tr=(e,r,t)=>er.requestFirst(e,r,t),or=(e,r,t)=>er.requestCollect(e,r,t),nr=async e=>{try{const r=await withTimeout(Q.rpc("list_pair_rooms",{p_code:e}),2500,"paired-room roster");return r?.error||!Array.isArray(r?.data)?{verified:!1,rooms:[]}:{verified:!0,rooms:r.data}}catch{return{verified:!1,rooms:[]}}},sr=(e,r)=>{if(!e||ye.has(e))return;const o=q(r);if(!o)return;const n=Q.channel(o,{config:{private:!0,broadcast:{self:!1}}}),s={channel:n,key:r,status:G.JOINING};n.on("broadcast",{event:"peer-list-req"},({payload:r})=>((e,r,t)=>{if("1"===process.env.TP_PAIRBUS_OFF||"1"===process.env.TP_CROSSROOM_OFF)return;const o=[...ce.keys()].filter(r=>he.get(r)===e).map(e=>({code:e,name:me.get(e)||null,host:Re}));try{r.send({type:"broadcast",event:"peer-list-res",payload:{nonce:t?.nonce,rooms:o}})}catch{}})(e,n,r)).on("broadcast",{event:"peer-list-res"},({payload:e})=>rr(e)).on("broadcast",{event:"peer-peek-req"},({payload:r})=>((e,r,o)=>{const n=e=>{try{r.send({type:"broadcast",event:"peer-peek-res",payload:{nonce:o?.nonce,...e}})}catch{}};if("1"===process.env.TP_PAIRBUS_OFF||"1"===process.env.TP_CROSSROOM_OFF)return n({error:"Cross-session reach is off on this device."});const s=String(o?.targetRoom||"").toUpperCase().trim(),i=ce.get(s);if(!i||he.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();fe.set(a,{reply:e=>n({text:e.text,error:e.error}),askerRoom:null});try{i.send({t:"pair-peek-local",reqId:a,terminal:o.terminal,lines:o.lines})}catch{fe.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-peek-res"},({payload:e})=>rr(e)).on("broadcast",{event:"peer-post-req"},({payload:r})=>((e,r,o)=>{const n=e=>{try{r.send({type:"broadcast",event:"peer-post-res",payload:{nonce:o?.nonce,...e}})}catch{}};if("1"===process.env.TP_PAIRBUS_OFF)return n({error:"Cross-room posting is off on this device."});if(!(e=>{const r=Date.now();let t=ve.get(e);return(!t||r>=t.resetAt)&&(t={count:0,resetAt:r+6e4},ve.set(e,t)),!(t.count>=6||(t.count++,0))})(e))return n({error:"Too many cross-room posts to this device right now — try again shortly."});const s=String(o?.targetRoom||"").toUpperCase().trim(),i=ce.get(s);if(!i||he.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();fe.set(a,{reply:e=>n({ok:e.ok,ref:e.ref,error:e.error}),askerRoom:null});try{i.send({t:"pair-post-local",reqId:a,fromRoom:o.fromRoom,fromHost:o.fromHost,fromTerminalName:o.fromTerminalName,text:o.text,terminal:o.terminal})}catch{fe.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-post-res"},({payload:e})=>rr(e)).subscribe(e=>{s.status=B(e),"SUBSCRIBED"===e?process.stderr.write("\n ◆ pair bus subscribed — cross-machine Ensemble reach with your partner.\n"):"CHANNEL_ERROR"!==e&&"TIMED_OUT"!==e||process.stderr.write(`\n ◇ pair bus ${e.toLowerCase().replace("_"," ")} — partner sessions are offline until it reconnects.\n`)}),ye.set(e,s)};let ir=0;const ar=async()=>{const e=Date.now();if(!(ir&&e-ir<6e4)){ir=e;try{await cr()}finally{ir===e&&(ir=0)}}};Ae=()=>{ar().catch(()=>{})};const cr=async()=>{if(!await Qe()){if(ce.size)for(const[,e]of ce)try{e.kill("SIGTERM")}catch{}return void Ue()}let r=[],s=!1;const a=ee.user.id;try{const{data:e,error:t}=await Q.from("code_sessions").select("code,name,last_active_at,owner_id,participants,closed_at").eq("owner_id",a).is("closed_at",null).order("last_active_at",{ascending:!1}).limit(20);if(t)throw t;r=e||[],s=!0;const o=new Set(r.map(e=>e.code));for(const e of[...$e.keys()])o.has(e)||$e.delete(e)}catch{}if(s)for(const e of P({rooms:r,childRooms:ce.keys(),myUid:a})){const r=ce.get(e);if(r){console.log(`child_retire sup=${Se} room=${e} pid=${r.pid||"?"} reason=not-active-owned`);try{r.kill("SIGTERM")}catch{}}}const c=l(),d=u();for(const s of r){const r=s.code;if(!r||ce.has(r)){r&&$e.delete(r);continue}if(!A({ownerId:s.owner_id,myUid:a,closedAt:s.closed_at}))continue;const l=N({bound:c[r],remembered:d[r],defaultDir:f()||y()||oe,home:i.homedir()});if(l.refuse){if($e.set(r,l.reason),!ke.has(r)){ke.add(r);const e="home"===l.reason?`no project directory set — refusing to serve it from your home folder (${l.dir}); the agent would lose its session context and the room would show "${o.basename(l.dir)}" as the repo.`:"no directory to serve it from.";process.stderr.write(`\n ◇ ${r}: ${e}\n Bind it to its repo: ${V("bind",r,"<repo-dir>")}\n`)}continue}const p=l.dir;if(!n.existsSync(p)){$e.set(r,"dir-gone"),ke.has(r)||(ke.add(r),process.stderr.write(`\n ◇ ${r}: bound directory is gone (${p}) — re-bind it.\n`));continue}ke.delete(r),$e.delete(r),l.fromDefault&&m(r,p),me.set(r,s.name||null),process.stderr.write(`\n ◆ serving ${r}${s.name?` "${s.name}"`:""} → ${p}${c[r]?"":" (default)"}\n`);const u={...process.env,THINKPOOL_PAIR_ACCOUNT_CHILD:"1",TP_ACCOUNT_BRIDGE_ID:xe,TP_ACCOUNT_BRIDGE_FENCING:String(Je)};delete u.THINKPOOL_PAIR_AUTOUPDATE,te&&(u.TP_ACCESS_TOKEN=te);const h=ce.has(r),v=F(`spawn the room bridge for ${r}`);if(!v.ok){ke.has(`memory:${r}`)||(ke.add(`memory:${r}`),process.stderr.write(`\n ◇ ${r}: ${v.reason}\n`)),$e.set(r,"host-memory");continue}ke.delete(`memory:${r}`);const g=e(process.execPath,[Z,r,"--headless","--auto=claude"],{cwd:p,stdio:["inherit","inherit","inherit","ipc"],env:u});console.log(`child_spawn sup=${Se} room=${r} pid=${g.pid} dir=${p} had_prior=${h}`),ce.set(r,g),ue.delete(r),de.set(r,!1),le.set(r,!1),pe.set(r,!1),g.on("message",e=>{if(e)if("room-ready"===e.t)ue.add(r),console.log(`child_ready sup=${Se} room=${r} pid=${g.pid}`),Ue();else if("idle"===e.t)de.set(r,!!e.idle),le.set(r,null!=e.between?!!e.between:!!e.idle),pe.set(r,!!e.webPeer),gr();else if("busy"===e.t)de.set(r,!1),le.set(r,!1);else if("restart-probe-result"===e.t){const t=be.get(e.nonce);if(!t||!t.pending.has(r)||ce.get(r)!==g)return;t.pending.delete(r),t.ok=t.ok&&!0===e.between,le.set(r,!0===e.between),t.pending.size||(clearTimeout(t.timer),be.delete(e.nonce),t.resolve(t.ok))}else if("apply-update"===e.t)ge=!0,gr();else if("pair-list-req"===e.t)(async()=>{if("1"===process.env.TP_CROSSROOM_OFF){try{g.send({t:"pair-list-res",reqId:e.reqId,rooms:[],error:"Cross-session reach is turned off on this bridge."})}catch{}return}const t=[...ce.keys()].filter(e=>e!==r).map(e=>({code:e,name:me.get(e)||null}));let o=[],n=[];if("1"!==process.env.TP_PAIRBUS_OFF){const e=await Promise.all([nr(r),ye.size?or("peer-list-req",{},1200):Promise.resolve([])]);o=e[0].rooms,n=e[1]}const s=H({authorized:o,replies:n,myUid:ee.user.id,localCodes:ce.keys()});try{g.send({t:"pair-list-res",reqId:e.reqId,rooms:[...t,...s]})}catch{}})();else if("pair-peek-req"===e.t)(async()=>{const o=r=>{try{g.send({t:"pair-peek-res",reqId:e.reqId,...r})}catch{}};if("1"===process.env.TP_CROSSROOM_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const n=String(e.targetRoom||"").toUpperCase().trim();if(!n||n===r)return o({error:`${n||"That"} is not another of your active sessions.`});const s=ce.get(n);if(s){const i=t();fe.set(i,{reply:e=>o({text:e.text,error:e.error}),askerRoom:r});try{s.send({t:"pair-peek-local",reqId:i,terminal:e.terminal,lines:e.lines})}catch{fe.delete(i),o({error:`Could not reach session ${n}.`})}return}if("1"===process.env.TP_PAIRBUS_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const[i,a]=await Promise.all([nr(r),ye.size?or("peer-list-req",{},800):Promise.resolve([])]);if(i.verified&&!i.rooms.some(e=>e?.code===n&&e?.owner_id!==ee.user.id))return o({error:`Room ${n} is not shared with this pair.`});if(!ye.size)return o({error:i.verified?`Room ${n} is paired with you, but its host bridge is offline.`:`Session ${n} is not running on this machine, and no pair bus is open to reach it.`});if(!a.some(e=>e?.rooms?.some(e=>e?.code===n)))return o({error:`Room ${n} is paired with you, but its host bridge is offline.`});const c=await(d=n,l=e.terminal,p=e.lines,tr("peer-peek-req",{targetRoom:d,terminal:l,lines:p},M.busTimeoutMs));var d,l,p;if(!c)return o({error:`Room ${n} did not respond — it may be offline, or not shared with you.`});o({text:c.text,error:c.error})})();else if("pair-post-req"===e.t)(async()=>{const o=r=>{try{g.send({t:"pair-post-res",reqId:e.reqId,...r})}catch{}};if("1"===process.env.TP_PAIRBUS_OFF)return o({error:"Cross-room posting is turned off on this bridge."});const n=String(e.targetRoom||"").toUpperCase().trim();if(!n||n===r)return o({error:`${n||"That"} is not another of your sessions.`});const s=ce.get(n);if(s){const i=t();fe.set(i,{reply:e=>o({ok:e.ok,ref:e.ref,error:e.error}),askerRoom:r});try{s.send({t:"pair-post-local",reqId:i,fromRoom:r,fromHost:Re,fromTerminalName:e.fromTerminalName,text:e.text,terminal:e.terminal})}catch{fe.delete(i),o({error:`Could not reach session ${n}.`})}return}const[i,a]=await Promise.all([nr(r),ye.size?or("peer-list-req",{},1200):Promise.resolve([])]);if(i.verified&&!i.rooms.some(e=>e?.code===n&&e?.owner_id!==ee.user.id))return o({error:`Room ${n} is not shared with this pair.`});if(!ye.size)return o({error:i.verified?`Room ${n} is paired with you, but its host bridge is offline.`:`Room ${n} is not on this machine, and no pair bus is open to reach it.`});if(!a.some(e=>e?.rooms?.some(e=>e?.code===n)))return o({error:`Room ${n} is paired with you, but its host bridge is offline — no recipient approval card can be raised yet.`});const c=await(d=n,l=e.terminal,p=e.text,u=r,m=e.fromTerminalName,tr("peer-post-req",{targetRoom:d,terminal:l,text:p,fromRoom:u,fromHost:Re,fromTerminalName:m},18e4));var d,l,p,u,m;if(!c)return o({error:`Room ${n} did not respond to the hand-off in time.`});o({ok:c.ok,ref:c.ref,error:c.error})})();else if("pair-peek-res"===e.t||"pair-post-res"===e.t){const r=fe.get(e.reqId);if(!r)return;fe.delete(e.reqId);try{r.reply(e)}catch{}}});const w=e=>{console.log(`child_exit sup=${Se} room=${r} pid=${g?.pid||"?"} code=${e||"?"}`),ce.delete(r),ue.delete(r),de.delete(r),le.delete(r),pe.delete(r),me.delete(r),he.delete(r);for(const[e,t]of fe)t?.askerRoom===r&&fe.delete(e)};if(g.on("exit",e=>w(e)),g.on("error",e=>{w();try{process.stderr.write(`\n ◇ ${r}: failed to launch its bridge (${e?.message||e}) — retrying next tick.\n`)}catch{}}),we)try{g.send({t:"update-available",version:we})}catch{}}if("1"!==process.env.TP_PAIRBUS_OFF)for(const e of r){if(!e?.code||!ce.has(e.code))continue;const r=Array.isArray(e.participants)?e.participants:[],t=2===r.length?r.find(e=>e&&e!==ee.user.id):null;if(!t){he.delete(e.code);continue}he.set(e.code,t);const o=E(ee.user.id,t);o&&sr(t,o)}Ue()};await ar();const dr=setInterval(ar,15e3);Le=!0,Be(),c();const lr=[];let pr=!1,ur=0,mr=0,fr=!1;const hr=setInterval(async()=>{if(pr)return;const e=Date.now();if(!claimLoopWedged({lastClaimOkAt:Xe,now:e}))return ur=0,void(mr=0);if(fr||e<mr)return;fr=!0;const r=b(ur);mr=e+r,ur+=1,console.log(`claim_watchdog sup=${Se} quiet_ms=${e-Xe} attempt=${ur} next_ms=${r} — reconnecting realtime + re-claiming`);try{Q.realtime?.connect?.()}catch{}ir=0;try{await Qe(),Ue()}catch{}finally{fr=!1}},2e4);hr.unref?.();const yr=setInterval(()=>{if(pr)return;const e=Date.now(),r=w({claimHeld:We,lastClaimOkAt:Xe,now:e});let t=!1;try{t=!!Ie.presenceState?.()[Re]}catch{t=!1}const o=_({tracking:r,subscribedAt:Math.max(Fe.subscribedAt,Xe),syncSeenAt:Fe.syncSeenAt,present:t,missingSince:Ye,now:e});if(Ye=o.missingSince,!Ye)return void(Ze=0);const n=o.verdict;if("recycle"===n){if(Ze===Ye)return;Ze=Ye,console.log(`presence_watchdog sup=${Se} action=recycle missing_ms=${e-Ye} track=${Ne} — full realtime disconnect+reconnect`);try{Q.realtime?.disconnect?.()}catch{}try{Q.realtime?.connect?.()}catch{}try{Ue()}catch{}}else"respawn"===n&&(console.log(`presence_watchdog sup=${Se} action=respawn missing_ms=${e-Ye} track=${Ne} — recycle failed, exiting for a clean supervisor respawn`),process.exit(1))},15e3);yr.unref?.();const vr=e=>{if(pr)return;pr=!0;const r=Ce?1:0;console.log(`supervisor_stop sup=${Se} reason=${e||"unknown"} restarting=${Ce} children=${ce.size}`),clearInterval(dr),clearInterval(hr),clearInterval(yr);for(const e of lr)try{clearInterval(e)}catch{}Ke.cancel(),z({pid:process.pid}),releaseSingleton();for(const r of ce.values())try{r.kill(e||"SIGTERM")}catch{}er.close();for(const{channel:e}of ye.values())try{Q.removeChannel(e)}catch{}(async()=>{try{We&&await Q.rpc("release_bridge",{p_bridge_id:xe})}catch{}try{await Ie.untrack()}catch{}try{await Q.removeChannel(Oe)}catch{}try{await Q.removeChannel(Ie)}catch{}process.exit(r)})(),setTimeout(()=>process.exit(r),1500)};for(const e of["SIGINT","SIGTERM","SIGHUP"])process.on(e,()=>vr(e));async function gr(){if(!_e&&v({pendingUpdate:we,stopping:pr,childIdle:de,childBetween:le,childPeer:pe,requested:ge})){if(_e=!0,!await function(e=2500){const r=[...ce.entries()];if(!r.length)return Promise.resolve(!0);const o=t();return new Promise(t=>{const n={pending:new Set(r.map(([e])=>e)),ok:!0,resolve:t,timer:null};n.timer=setTimeout(()=>{be.delete(o),t(!1)},e),n.timer.unref?.(),be.set(o,n);for(const[e,t]of r)try{t.send({t:"restart-probe",nonce:o})}catch{n.pending.delete(e),n.ok=!1}n.pending.size||(clearTimeout(n.timer),be.delete(o),t(n.ok))})}())return _e=!1,void process.stderr.write("\n ◆ bridge update still queued — an agent became active before the restart boundary.\n");process.stderr.write(`\n ◆ thinkpool-pair ${we} ready (running ${Y}) — restarting account bridge to update; sessions resume.\n`);try{const e=await import("./service.mjs");if(e.serviceActive(null)&&!1!==e.applyStagedServiceUpdate(null,we))return}catch{}if(_e=!1,Ce=!0,!se())try{e(process.execPath,[process.argv[1]],{detached:!0,stdio:"ignore",env:process.env}).unref()}catch{}vr("SIGTERM")}}if("1"===process.env.THINKPOOL_PAIR_AUTOUPDATE&&Y){const e=1e3*Math.max(60,parseInt(process.env.THINKPOOL_PAIR_UPDATE_INTERVAL,10)||1800),r=(e,r)=>{const t=e=>String(e).split("-")[0].split(".").map(e=>parseInt(e,10)||0),o=t(e),n=t(r);for(let e=0;e<3;e++){if((o[e]||0)>(n[e]||0))return!0;if((o[e]||0)<(n[e]||0))return!1}return!1},t=async()=>{const e=new AbortController,r=setTimeout(()=>e.abort(),8e3);try{const r=await fetch("https://registry.npmjs.org/thinkpool-pair/latest",{signal:e.signal,headers:{accept:"application/json"}});if(!r.ok)return null;const t=await r.json();return"string"==typeof t?.version?t.version:null}catch{return null}finally{clearTimeout(r)}},o=async()=>{const e=await t();if(e&&r(e,Y)){we||process.stderr.write(`\n ◆ thinkpool-pair ${e} published — "update ready" surfaced to each room; applies between turns.\n`);try{(await import("./service.mjs")).provisionRuntime(e)}catch(r){return void process.stderr.write(`\n ⚠ bridge ${e} could not be staged yet: ${r?.message||r}\n`)}we=e;for(const r of ce.values())try{r.send({t:"update-available",version:e})}catch{}gr()}};lr.push(setInterval(o,e).unref()),lr.push(setInterval(gr,15e3).unref()),lr.push(setTimeout(o,6e4).unref())}await new Promise(()=>{})}
1
+ import{spawn as e}from"node:child_process";import{fileURLToPath as r}from"node:url";import{randomUUID as t}from"node:crypto";import o from"node:path";import n from"node:fs";import{createClient as s}from"@supabase/supabase-js";import i from"node:os";import{saveAuth as a,loadAuth as c,markAuthReconnectRequired as d,loadDirs as l,bindDir as p,loadServed as u,rememberServed as m,loadDefaultDir as f,saveDefaultDir as h,claudeRecentProjectDir as y}from"./auth-store.mjs";import{isSafeToRestart as v}from"./update-gate.mjs";import{makeThrottledTrack as g,presenceRecoveryEligible as w,presenceSelfEchoObservation as _,recoveryBackoffMs as b,reducePresenceChannelEvidence as k,subscribeWithoutStartupDeadlock as $}from"./presence.mjs";import{publicKeyB64 as R,announceProviders as S,listProviders as T,addProvider as I,addProviderModel as O,removeProvider as x,unseal as C}from"./providers.mjs";import{supervisorServes as A,supervisorRoomsToStop as P}from"./serve-consent.mjs";import{resolveServeDir as N}from"./serve-dir.mjs";import{pairKeyFor as E,pairTopic as q,CROSSROOM_BUS as M}from"./cross-terminal.mjs";import{installedAgentCommands as U}from"./agent-detect.mjs";import{agentVisibilityStates as D,setAgentPickerVisibility as j}from"./agent-visibility.mjs";import{hostMemoryAdmission as F}from"./host-memory.mjs";import{createPairBusBroker as L,mergePairRoomRoster as H,pairBusStatusFromRealtime as B,PAIR_BUS_STATUS as G}from"./pair-bus.mjs";import{clearSupervisorReady as z,supervisorPresenceEchoed as K,writeSupervisorReady as W}from"./supervisor-ready.mjs";import{PAIR_CLI as J,pairCli as V}from"./command-guidance.mjs";import{syncRealtimeAuth as X}from"./design-edit.mjs";const Y=(()=>{try{return JSON.parse(n.readFileSync(new URL("./package.json",import.meta.url),"utf8")).version}catch{return null}})(),Z=r(new URL("./bridge.mjs",import.meta.url));export function createStartupAnchor({set:e=setInterval,clear:r=clearInterval}={}){const t=e(()=>{},6e4);let o=!1;return()=>{o||(o=!0,r(t))}}const Q=o.join(i.homedir(),".thinkpool-pair","account.lock");function ee(e){if(!e||e===process.pid)return!1;try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}export function acquireSingleton(e=Q){try{n.mkdirSync(o.dirname(e),{recursive:!0})}catch{return!1}for(let r=0;r<3;r++)try{const r=n.openSync(e,"wx");return n.writeSync(r,String(process.pid)),n.closeSync(r),!0}catch(r){if("EEXIST"!==r.code)return!1;let t=0;try{t=parseInt(n.readFileSync(e,"utf8").trim(),10)||0}catch{return!1}if(ee(t))return!1;try{n.rmSync(e,{force:!0})}catch{return!1}}return!1}export function releaseSingleton(e=Q){try{(parseInt(n.readFileSync(e,"utf8").trim(),10)||0)===process.pid&&n.rmSync(e,{force:!0})}catch{}}export function acquireMachineLock(e=Q){return acquireSingleton(e)}export function releaseMachineLock(e=Q){return releaseSingleton(e)}export function shouldHoldMachineLock(e=process.env){return"1"!==e.THINKPOOL_PAIR_ACCOUNT_CHILD}export async function runLogin(e,r,t){const o=s(e,r,{auth:{persistSession:!1}}),{data:n,error:i}=await o.rpc("start_device_code"),c=Array.isArray(n)?n[0]:n;!i&&c?.device_code||(process.stderr.write(`\n ✗ couldn't start login: ${i?.message||"no response"}\n`),process.exit(1));const{device_code:d,user_code:l}=c,p=`${t}/code/link?c=${encodeURIComponent(l)}`;process.stderr.write(`\n ◆ Link this device to your thinkpool account.\n Open (signed in) and approve:\n\n ${p}\n\n code: ${l}\n\n Waiting…\n`);const u=Date.now(),m=async()=>{Date.now()-u>3e5&&(process.stderr.write(`\n ◇ link timed out — run \`${V("login")}\` again.\n`),process.exit(1));try{const{data:e}=await o.rpc("claim_device_code",{p_device_code:d});"approved"===e?.status&&e.session?.refresh_token&&(a(e.session),process.stderr.write(`\n ◆ linked as ${e.session.email||"your account"}. You can close the browser tab.\n Now run: ${J}\n`),process.exit(0)),"expired"!==e?.status&&"not_found"!==e?.status||(process.stderr.write(`\n ◇ link expired — run \`${V("login")}\` again.\n`),process.exit(1))}catch{}setTimeout(m,3e3)};m(),await new Promise(()=>{})}const re=e=>new Promise(r=>{const t=setTimeout(r,e);t.unref?.()});export function withTimeout(e,r,t="op"){return new Promise((o,n)=>{const s=setTimeout(()=>n(new Error(`${t} timed out after ${r}ms`)),r);s.unref?.(),Promise.resolve(e).then(e=>{clearTimeout(s),o(e)},e=>{clearTimeout(s),n(e)})})}export function savedTokenStillValid(e,r=Math.floor(Date.now()/1e3),t=300){if(!e?.access_token)return!1;const o=e.expires_at||function(e){try{const r=String(e).split(".")[1],t=Buffer.from(r.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"),o=JSON.parse(t)?.exp;return"number"==typeof o?o:0}catch{return 0}}(e.access_token);return!!o&&o-r>t}const te=o.join(i.homedir(),".thinkpool-pair","refresh.lock");export function acquireRefreshLock(e=te,r=3e4){try{n.mkdirSync(o.dirname(e),{recursive:!0})}catch{}for(let t=0;t<2;t++)try{const r=n.openSync(e,"wx");return n.writeSync(r,JSON.stringify({pid:process.pid,at:Date.now()})),n.closeSync(r),!0}catch(t){if("EEXIST"!==t.code)return!0;let o=1/0;try{o=Date.now()-(JSON.parse(n.readFileSync(e,"utf8"))?.at||0)}catch{o=1/0}if(o<r)return!1;try{n.rmSync(e,{force:!0})}catch{return!0}}return!1}export function releaseRefreshLock(e=te){try{n.rmSync(e,{force:!0})}catch{}}async function oe(e,r){if(!r?.access_token||!r?.refresh_token)return null;try{const{data:t,error:o}=await e.auth.setSession({access_token:r.access_token,refresh_token:r.refresh_token});return o||!t?.session?null:t.session}catch{return null}}export async function refreshAccountSession(e,r,t={}){const{load:o=c,save:n=a,acquire:s=acquireRefreshLock,release:i=releaseRefreshLock,wait:d=re}=t,l=async r=>{const t=o();if(t?.refresh_token&&r&&t.refresh_token!==r){const r=await oe(e,t);if(r)return{session:r,adopted:!0}}return null};let p=await l(r);if(p)return p;const u=o()?.refresh_token||r;if(!u)return null;let m=s();if(!m){for(let e=0;e<20;e++){if(await d(250),p=await l(u),p)return p;if(s()){m=!0;break}}if(!m)return null}try{if(p=await l(u),p)return p;const r=o()?.refresh_token||u,{data:t,error:s}=await e.auth.refreshSession({refresh_token:r});return s||!t?.session?{session:null,error:s||null}:(n(t.session),{session:t.session,adopted:!1})}finally{m&&i()}}export const ACCOUNT_RECONNECT_ERROR_CODES=new Set(["refresh_token_not_found","refresh_token_already_used","session_not_found"]);export function accountRefreshNeedsReconnect(e){return ACCOUNT_RECONNECT_ERROR_CODES.has(String(e?.code||""))}export async function authedClient(e,r){const t=c();if(!t?.refresh_token)return null;if(t.reconnect_required)return{reconnectRequired:!0};const o=s(e,r,{auth:{persistSession:!1,autoRefreshToken:!1}});if(savedTokenStillValid(t)){const e=await oe(o,t);if(e)return{sb:o,session:e}}const n=await refreshAccountSession(o,t.refresh_token);return n?.session?{sb:o,session:n.session}:accountRefreshNeedsReconnect(n?.error)?(d(n.error.code),{reconnectRequired:!0}):null}export async function authedClientWithRetry(e,r,t={}){const{attempt:o=authedClient,load:n=c,wait:s=re,onRetry:i=null,maxAttempts:a=1/0,baseMs:d=15e3,capMs:l=3e5}=t;let p=d;for(let t=1;t<=a;t++){const c=await o(e,r);if(c?.reconnectRequired)return c;if(c)return c;if(!n()?.refresh_token)return null;if(t>=a)break;i?.(p,t),await s(p),p=Math.min(2*p,l)}return null}export function claimLoopWedged({lastClaimOkAt:e,now:r,quietMsThreshold:t=4e4}){return r-e>=t}export function scheduleTokenRefresh({sb:e,session:r,onRefreshed:t,onReconnectRequired:o,skewMs:n=9e4,forceMs:s=0,retryMs:i=3e4,refresh:a=refreshAccountSession}){let c=null,d=r,l=!1,p=!1;const u=e=>{l||(clearTimeout(c),c=setTimeout(f,Math.max(1e3,e)),c?.unref&&c.unref())},m=()=>{if(s)return s;const e=d?.expires_at?1e3*d.expires_at:Date.now()+36e5;return Math.max(3e4,e-Date.now()-n)};async function f(){if(!l)try{const r=await a(e,d.refresh_token);if(!r?.session){if(accountRefreshNeedsReconnect(r?.error)&&!p){p=!0;try{await(o?.(r.error))}catch{}}throw r?.error||new Error("no session")}d=r.session;try{e.realtime?.setAuth?.(d.access_token)}catch{}p=!1;try{await(t?.(d))}catch{}u(m())}catch{u(i)}}return u(m()),{cancel:()=>{l=!0,clearTimeout(c)},_tick:f}}export function runBind(e,r){const t=(e||"").toUpperCase().trim();t||(console.error(`usage: ${V("bind","<ROOM>","<directory>")}`),process.exit(1));const s=o.resolve(r||".");n.existsSync(s)&&n.statSync(s).isDirectory()||(console.error(`✗ not a directory: ${s}`),process.exit(1)),p(t,s),console.error(`✓ bound ${t} → ${s}\n Run \`${J}\` (or restart it) to serve this session.`),process.exit(0)}export async function runAccount(r,s){acquireSingleton()||(console.error(`\n ◇ An account bridge is already running on this machine\n (~/.thinkpool-pair/account.lock). Not starting a second — two would race\n your saved login. Stop the other one first, or share a single room with\n \`${V("<ROOM>")}\`.\n`),process.exit(0)),z();const c=createStartupAnchor(),J=await authedClientWithRetry(r,s,{onRetry:e=>process.stderr.write(`\n ◇ Couldn't refresh your account session yet (offline, or another bridge is mid-rotation) — retrying in ${Math.round(e/1e3)}s. Saved login left in place.\n`)});J||(releaseSingleton(),console.error(`\n ◇ Not linked to a thinkpool account on this machine.\n Link it: ${V("login")}\n`),process.exit(0)),J.reconnectRequired&&(c(),releaseSingleton(),console.error("\n ◇ This bridge lost its thinkpool account link.\n Reconnect it: npx thinkpool-pair@latest login\n"),process.exit(0));const{sb:Q,session:ee}=J,re=ee.user?.email||"your account";let te=ee.access_token||null;const oe=process.cwd();let ne=!1;try{const e=await import("./service.mjs");ne=e.serviceActive(null)}catch{}const se=()=>"1"===process.env.THINKPOOL_PAIR_AUTOUPDATE||ne,ie="1"===process.env.THINKPOOL_PAIR_AUTOUPDATE?" (auto-updating background service — survives reboot, tracks @latest)\n":ne?" (background service — survives reboot, restarts on crash, pinned to this version)\n":` (run hands-off + auto-updating: ${V("install-service")})\n`,ae=f()||y()||oe;process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${re}\n New sessions auto-serve from ${ae}${f()?" (your default)":y()?" (your most-recent Claude Code project)":""}\n (change it: ${V("set-default-dir","<dir>")} · one room: ${V("bind","<code>","<dir>")})\n${ie}`);const ce=new Map,de=new Map,le=new Map,pe=new Map,ue=new Set,me=new Map,fe=new Map,he=new Map,ye=new Map,ve=new Map;let ge=!1,we=null,_e=!1;const be=new Map,ke=new Set,$e=new Map,Re=i.hostname().replace(/\.local$/,""),Se=(t?.()||"").slice(0,8);let Te=null;console.log(`◇ supervisor boot sup=${Se} host=${Re} pid=${process.pid} npx=${Y||"dev"}`),await X(Q,te,s)||process.stderr.write("\n ⚠ realtime auth could not be initialized; private bridge control is unavailable.\n");const Ie=Q.channel(`tpacct:${ee.user.id}`,{config:{presence:{key:Re},broadcast:{self:!1}}}),Oe=Q.channel(`tpacct-control:${ee.user.id}`,{config:{private:!0,broadcast:{self:!1}}}),xe=t();let Ce=!1,Ae=()=>{};Oe.on("broadcast",{event:"bind-room"},({payload:e})=>{const r=String(e?.code||"").toUpperCase().trim();let t=String(e?.dir||"").trim();t.startsWith("~")&&(t=o.join(i.homedir(),t.slice(1))),r&&t&&(n.existsSync(t)?(p(r,t),e?.setDefault&&(h(t),process.stderr.write(`\n ◆ default project dir set → ${t}\n`)),process.stderr.write(`\n ◆ ${r} bound → ${t} (from the web). Serving…\n`),Ae()):process.stderr.write(`\n ◇ bind ${r}: directory not found (${t}) — ignored.\n`))}),Oe.on("broadcast",{event:"serve-room"},({payload:e})=>{const r=String(e?.code||"").toUpperCase().trim();/^[A-Z0-9]{4,12}$/.test(r)&&Ae()}),Oe.on("broadcast",{event:"shutdown"},async()=>{if(process.stderr.write("\n ◇ disconnect requested from the dashboard — stopping bridge.\n"),setTimeout(()=>{try{releaseSingleton()}catch{}process.exit(0)},2500),se())try{(await import("./service.mjs")).uninstallService(null)}catch{}process.kill(process.pid,"SIGTERM")});let Pe=!1;Oe.on("broadcast",{event:"restart"},async({payload:e}={})=>{const r=e?.nonce,t=async(e,t)=>{try{await Oe.send({type:"broadcast",event:"restart-status",payload:{nonce:r,ok:e,error:t}})}catch{}};if(Pe||_e)await t(!1,"a bridge update is already being prepared");else{Pe=!0;try{const r=await import("./service.mjs"),o=r.serviceActive(null),n=o?r.stageServiceUpdate(null):null;if(o&&!n)return void await t(!1,"the published runtime could not be staged; the existing bridge was left running");we=n||("string"==typeof e?.version?e.version:Y)||"restart",ge=!0,await t(!0)}catch(e){return void await t(!1,`restart preparation failed: ${e?.message||e}`)}finally{Pe=!1}gr()}});let Ne="init",Ee="connected";const qe=g(Ie,{minMs:5e3,onStatus:e=>{Ne=e}}),Me=R(),Ue=()=>qe({name:Re,bridgeId:xe,version:Y,rooms:[...ce.keys()],readyRooms:[...ue],refused:[...$e].map(([e,r])=>({code:e,reason:r})),service:se(),providerPubKey:Me,providers:S(),agents:D(U()),accountAuthState:Ee,ts:Date.now()}),De=async e=>{try{await withTimeout(Q.rpc("report_bridge_auth_state",{p_bridge_id:xe,p_state:e}),5e3,"report_bridge_auth_state")}catch{}},je=(e,r)=>{try{Oe.send({type:"broadcast",event:e,payload:r})}catch{}};Oe.on("broadcast",{event:"provider-add"},async({payload:e})=>{const r=e?.nonce;let t;try{t=C(e?.sealed)}catch{return je("provider-add-res",{nonce:r,ok:!1,error:"could not decrypt the sealed payload"})}const o=I(t);o.ok&&(Ue(),process.stderr.write(`\n ◆ provider added (${String(t?.name||"").slice(0,40)}) — re-announced.\n`)),je("provider-add-res",{nonce:r,ok:o.ok,error:o.ok?void 0:o.error,id:o.ok?o.id:void 0})}),Oe.on("broadcast",{event:"provider-add-model"},async({payload:e})=>{const r=e?.nonce,t=O({id:e?.id,model:e?.model,name:e?.name});t.ok&&(Ue(),process.stderr.write(`\n ◆ model added to an existing provider (${String(e?.model||"").slice(0,40)}) — re-announced.\n`)),je("provider-add-model-res",{nonce:r,ok:t.ok,error:t.ok?void 0:t.error,id:t.ok?t.id:void 0})}),Oe.on("broadcast",{event:"provider-remove"},async({payload:e})=>{const r=e?.nonce,t=x(e?.id);t.ok&&(Ue(),process.stderr.write("\n ◆ provider removed — re-announced.\n")),je("provider-remove-res",{nonce:r,ok:t.ok,error:t.ok?void 0:t.error})}),Oe.on("broadcast",{event:"providers-list-req"},async({payload:e})=>{const r=e?.nonce;je("providers-list-res",{nonce:r,ok:!0,providers:T()})}),Oe.on("broadcast",{event:"agent-visibility-set"},async({payload:e})=>{const r=e?.nonce,t=U(),o=j(e?.cmd,!0===e?.enabled,{installed:t});if(o.ok){Ue();for(const e of ce.values())try{e.send({t:"agent-visibility-changed"})}catch{}process.stderr.write(`\n ◆ terminal picker agents updated — ${o.agents.filter(e=>e.enabled).map(e=>e.cmd).join(", ")} visible.\n`)}je("agent-visibility-res",{nonce:r,ok:o.ok,error:o.ok?void 0:o.error,agents:o.ok?o.agents:void 0})});let Fe=k(),Le=!1,He=!1;const Be=()=>{if(He||!Le)return;let e=!1;try{e=K(Ie.presenceState?.(),Re,xe,Y)}catch{e=!1}e&&(He=W({version:Y,bridgeId:xe}),He||process.stderr.write("\n ◇ Could not record local supervisor readiness; the bridge remains running but installers will not claim it was confirmed.\n"))};Ie.on("presence",{event:"sync"},()=>{Fe=k(Fe,"sync"),Be()});const Ge=await $(Ie,e=>{if("SUBSCRIBED"===e)Fe=k(Fe,e),Ue();else if("CLOSED"===e||"CHANNEL_ERROR"===e||"TIMED_OUT"===e){Fe=k(Fe,e);try{Q.realtime?.connect?.()}catch{}}});Ge.connected||process.stderr.write(`\n ◇ account realtime not ready at startup (${Ge.status}) — staying alive and recovering in process.\n`);const ze=await $(Oe,e=>{"CLOSED"!==e&&"CHANNEL_ERROR"!==e&&"TIMED_OUT"!==e||process.stderr.write(`\n ⚠ private account control ${e}; host-changing dashboard actions are unavailable until Realtime reconnects.\n`)});ze.connected||process.stderr.write(`\n ◇ private account control not ready at startup (${ze.status}) — presence remains available.\n`);const Ke=scheduleTokenRefresh({sb:Q,session:ee,onRefreshed:async e=>{Ee="connected",a(e),Ue(),await De("connected"),te=e.access_token||te;for(const e of ce.values())try{e.send({t:"token",accessToken:te})}catch{}},onReconnectRequired:async e=>{Ee="reconnect-required",d(e?.code||"refresh-token-invalid"),Ue(),await De("reconnect-required"),process.stderr.write("\n ◇ This bridge lost its thinkpool account link.\n Reconnect it: npx thinkpool-pair@latest login\n")},forceMs:parseInt(process.env.TP_TOKEN_REFRESH_FORCE_MS,10)||0});let We=!1,Je=0,Ve=null,Xe=Date.now(),Ye=0,Ze=0;const Qe=async()=>{try{const{data:e}=await withTimeout(Q.rpc("claim_bridge",{p_bridge_id:xe,p_hostname:Re}),1e4,"claim_bridge");Xe=Date.now();const r=Array.isArray(e)?e[0]:e,t=!!r?.held,o=r?.holder_host||"?";return t&&!We?process.stderr.write("\n ◆ holding this account — serving your sessions (single active bridge).\n"):!t&&We?process.stderr.write(`\n ◇ another device (${o}) took over this account — standing by.\n`):t||Ve===o||process.stderr.write(`\n ◇ another device (${o}) is serving this account — standing by.\n (this Mac takes over automatically if that device goes offline.)\n`),We=t,Je=t&&Number.isSafeInteger(Number(r?.fencing))&&Number(r.fencing)>0?Number(r.fencing):0,Ve=t?null:o,Te!==t&&(console.log(`claim_tick sup=${Se} held=${t} holder=${t?Re:o}`),Te=t),t}catch{return We}},er=L({channels:ye,randomId:t}),rr=e=>er.resolve(e),tr=(e,r,t)=>er.requestFirst(e,r,t),or=(e,r,t)=>er.requestCollect(e,r,t),nr=async e=>{try{const r=await withTimeout(Q.rpc("list_pair_rooms",{p_code:e}),2500,"paired-room roster");return r?.error||!Array.isArray(r?.data)?{verified:!1,rooms:[]}:{verified:!0,rooms:r.data}}catch{return{verified:!1,rooms:[]}}},sr=(e,r)=>{if(!e||ye.has(e))return;const o=q(r);if(!o)return;const n=Q.channel(o,{config:{private:!0,broadcast:{self:!1}}}),s={channel:n,key:r,status:G.JOINING};n.on("broadcast",{event:"peer-list-req"},({payload:r})=>((e,r,t)=>{if("1"===process.env.TP_PAIRBUS_OFF||"1"===process.env.TP_CROSSROOM_OFF)return;const o=[...ce.keys()].filter(r=>he.get(r)===e).map(e=>({code:e,name:me.get(e)||null,host:Re}));try{r.send({type:"broadcast",event:"peer-list-res",payload:{nonce:t?.nonce,rooms:o}})}catch{}})(e,n,r)).on("broadcast",{event:"peer-list-res"},({payload:e})=>rr(e)).on("broadcast",{event:"peer-peek-req"},({payload:r})=>((e,r,o)=>{const n=e=>{try{r.send({type:"broadcast",event:"peer-peek-res",payload:{nonce:o?.nonce,...e}})}catch{}};if("1"===process.env.TP_PAIRBUS_OFF||"1"===process.env.TP_CROSSROOM_OFF)return n({error:"Cross-session reach is off on this device."});const s=String(o?.targetRoom||"").toUpperCase().trim(),i=ce.get(s);if(!i||he.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();fe.set(a,{reply:e=>n({text:e.text,error:e.error}),askerRoom:null});try{i.send({t:"pair-peek-local",reqId:a,terminal:o.terminal,lines:o.lines})}catch{fe.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-peek-res"},({payload:e})=>rr(e)).on("broadcast",{event:"peer-post-req"},({payload:r})=>((e,r,o)=>{const n=e=>{try{r.send({type:"broadcast",event:"peer-post-res",payload:{nonce:o?.nonce,...e}})}catch{}};if("1"===process.env.TP_PAIRBUS_OFF)return n({error:"Cross-room posting is off on this device."});if(!(e=>{const r=Date.now();let t=ve.get(e);return(!t||r>=t.resetAt)&&(t={count:0,resetAt:r+6e4},ve.set(e,t)),!(t.count>=6||(t.count++,0))})(e))return n({error:"Too many cross-room posts to this device right now — try again shortly."});const s=String(o?.targetRoom||"").toUpperCase().trim(),i=ce.get(s);if(!i||he.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();fe.set(a,{reply:e=>n({ok:e.ok,ref:e.ref,error:e.error}),askerRoom:null});try{i.send({t:"pair-post-local",reqId:a,fromRoom:o.fromRoom,fromHost:o.fromHost,fromTerminalName:o.fromTerminalName,text:o.text,terminal:o.terminal})}catch{fe.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-post-res"},({payload:e})=>rr(e)).subscribe(e=>{s.status=B(e),"SUBSCRIBED"===e?process.stderr.write("\n ◆ pair bus subscribed — cross-machine Ensemble reach with your partner.\n"):"CHANNEL_ERROR"!==e&&"TIMED_OUT"!==e||process.stderr.write(`\n ◇ pair bus ${e.toLowerCase().replace("_"," ")} — partner sessions are offline until it reconnects.\n`)}),ye.set(e,s)};let ir=0;const ar=async()=>{const e=Date.now();if(!(ir&&e-ir<6e4)){ir=e;try{await cr()}finally{ir===e&&(ir=0)}}};Ae=()=>{ar().catch(()=>{})};const cr=async()=>{if(!await Qe()){if(ce.size)for(const[,e]of ce)try{e.kill("SIGTERM")}catch{}return void Ue()}let r=[],s=!1;const a=ee.user.id;try{const{data:e,error:t}=await Q.from("code_sessions").select("code,name,last_active_at,owner_id,participants,closed_at").eq("owner_id",a).is("closed_at",null).order("last_active_at",{ascending:!1}).limit(20);if(t)throw t;r=e||[],s=!0;const o=new Set(r.map(e=>e.code));for(const e of[...$e.keys()])o.has(e)||$e.delete(e)}catch{}if(s)for(const e of P({rooms:r,childRooms:ce.keys(),myUid:a})){const r=ce.get(e);if(r){console.log(`child_retire sup=${Se} room=${e} pid=${r.pid||"?"} reason=not-active-owned`);try{r.kill("SIGTERM")}catch{}}}const c=l(),d=u();for(const s of r){const r=s.code;if(!r||ce.has(r)){r&&$e.delete(r);continue}if(!A({ownerId:s.owner_id,myUid:a,closedAt:s.closed_at}))continue;const l=N({bound:c[r],remembered:d[r],defaultDir:f()||y()||oe,home:i.homedir()});if(l.refuse){if($e.set(r,l.reason),!ke.has(r)){ke.add(r);const e="home"===l.reason?`no project directory set — refusing to serve it from your home folder (${l.dir}); the agent would lose its session context and the room would show "${o.basename(l.dir)}" as the repo.`:"no directory to serve it from.";process.stderr.write(`\n ◇ ${r}: ${e}\n Bind it to its repo: ${V("bind",r,"<repo-dir>")}\n`)}continue}const p=l.dir;if(!n.existsSync(p)){$e.set(r,"dir-gone"),ke.has(r)||(ke.add(r),process.stderr.write(`\n ◇ ${r}: bound directory is gone (${p}) — re-bind it.\n`));continue}ke.delete(r),$e.delete(r),l.fromDefault&&m(r,p),me.set(r,s.name||null),process.stderr.write(`\n ◆ serving ${r}${s.name?` "${s.name}"`:""} → ${p}${c[r]?"":" (default)"}\n`);const u={...process.env,THINKPOOL_PAIR_ACCOUNT_CHILD:"1",TP_ACCOUNT_BRIDGE_ID:xe,TP_ACCOUNT_BRIDGE_FENCING:String(Je)};delete u.THINKPOOL_PAIR_AUTOUPDATE,te&&(u.TP_ACCESS_TOKEN=te);const h=ce.has(r),v=F(`spawn the room bridge for ${r}`);if(!v.ok){ke.has(`memory:${r}`)||(ke.add(`memory:${r}`),process.stderr.write(`\n ◇ ${r}: ${v.reason}\n`)),$e.set(r,"host-memory");continue}ke.delete(`memory:${r}`);const g=e(process.execPath,[Z,r,"--headless","--auto=claude"],{cwd:p,stdio:["inherit","inherit","inherit","ipc"],env:u});console.log(`child_spawn sup=${Se} room=${r} pid=${g.pid} dir=${p} had_prior=${h}`),ce.set(r,g),ue.delete(r),de.set(r,!1),le.set(r,!1),pe.set(r,!1),g.on("message",e=>{if(e)if("room-ready"===e.t)ue.add(r),console.log(`child_ready sup=${Se} room=${r} pid=${g.pid}`),Ue();else if("idle"===e.t)de.set(r,!!e.idle),le.set(r,null!=e.between?!!e.between:!!e.idle),pe.set(r,!!e.webPeer),gr();else if("busy"===e.t)de.set(r,!1),le.set(r,!1);else if("restart-probe-result"===e.t){const t=be.get(e.nonce);if(!t||!t.pending.has(r)||ce.get(r)!==g)return;t.pending.delete(r),t.ok=t.ok&&!0===e.between,le.set(r,!0===e.between),t.pending.size||(clearTimeout(t.timer),be.delete(e.nonce),t.resolve(t.ok))}else if("apply-update"===e.t)ge=!0,gr();else if("pair-list-req"===e.t)(async()=>{if("1"===process.env.TP_CROSSROOM_OFF){try{g.send({t:"pair-list-res",reqId:e.reqId,rooms:[],error:"Cross-session reach is turned off on this bridge."})}catch{}return}const t=[...ce.keys()].filter(e=>e!==r).map(e=>({code:e,name:me.get(e)||null}));let o=[],n=[];if("1"!==process.env.TP_PAIRBUS_OFF){const e=await Promise.all([nr(r),ye.size?or("peer-list-req",{},1200):Promise.resolve([])]);o=e[0].rooms,n=e[1]}const s=H({authorized:o,replies:n,myUid:ee.user.id,localCodes:ce.keys()});try{g.send({t:"pair-list-res",reqId:e.reqId,rooms:[...t,...s]})}catch{}})();else if("pair-peek-req"===e.t)(async()=>{const o=r=>{try{g.send({t:"pair-peek-res",reqId:e.reqId,...r})}catch{}};if("1"===process.env.TP_CROSSROOM_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const n=String(e.targetRoom||"").toUpperCase().trim();if(!n||n===r)return o({error:`${n||"That"} is not another of your active sessions.`});const s=ce.get(n);if(s){const i=t();fe.set(i,{reply:e=>o({text:e.text,error:e.error}),askerRoom:r});try{s.send({t:"pair-peek-local",reqId:i,terminal:e.terminal,lines:e.lines})}catch{fe.delete(i),o({error:`Could not reach session ${n}.`})}return}if("1"===process.env.TP_PAIRBUS_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const[i,a]=await Promise.all([nr(r),ye.size?or("peer-list-req",{},800):Promise.resolve([])]);if(i.verified&&!i.rooms.some(e=>e?.code===n&&e?.owner_id!==ee.user.id))return o({error:`Room ${n} is not shared with this pair.`});if(!ye.size)return o({error:i.verified?`Room ${n} is paired with you, but its host bridge is offline.`:`Session ${n} is not running on this machine, and no pair bus is open to reach it.`});if(!a.some(e=>e?.rooms?.some(e=>e?.code===n)))return o({error:`Room ${n} is paired with you, but its host bridge is offline.`});const c=await(d=n,l=e.terminal,p=e.lines,tr("peer-peek-req",{targetRoom:d,terminal:l,lines:p},M.busTimeoutMs));var d,l,p;if(!c)return o({error:`Room ${n} did not respond — it may be offline, or not shared with you.`});o({text:c.text,error:c.error})})();else if("pair-post-req"===e.t)(async()=>{const o=r=>{try{g.send({t:"pair-post-res",reqId:e.reqId,...r})}catch{}};if("1"===process.env.TP_PAIRBUS_OFF)return o({error:"Cross-room posting is turned off on this bridge."});const n=String(e.targetRoom||"").toUpperCase().trim();if(!n||n===r)return o({error:`${n||"That"} is not another of your sessions.`});const s=ce.get(n);if(s){const i=t();fe.set(i,{reply:e=>o({ok:e.ok,ref:e.ref,error:e.error}),askerRoom:r});try{s.send({t:"pair-post-local",reqId:i,fromRoom:r,fromHost:Re,fromTerminalName:e.fromTerminalName,text:e.text,terminal:e.terminal})}catch{fe.delete(i),o({error:`Could not reach session ${n}.`})}return}const[i,a]=await Promise.all([nr(r),ye.size?or("peer-list-req",{},1200):Promise.resolve([])]);if(i.verified&&!i.rooms.some(e=>e?.code===n&&e?.owner_id!==ee.user.id))return o({error:`Room ${n} is not shared with this pair.`});if(!ye.size)return o({error:i.verified?`Room ${n} is paired with you, but its host bridge is offline.`:`Room ${n} is not on this machine, and no pair bus is open to reach it.`});if(!a.some(e=>e?.rooms?.some(e=>e?.code===n)))return o({error:`Room ${n} is paired with you, but its host bridge is offline — no recipient approval card can be raised yet.`});const c=await(d=n,l=e.terminal,p=e.text,u=r,m=e.fromTerminalName,tr("peer-post-req",{targetRoom:d,terminal:l,text:p,fromRoom:u,fromHost:Re,fromTerminalName:m},18e4));var d,l,p,u,m;if(!c)return o({error:`Room ${n} did not respond to the hand-off in time.`});o({ok:c.ok,ref:c.ref,error:c.error})})();else if("pair-peek-res"===e.t||"pair-post-res"===e.t){const r=fe.get(e.reqId);if(!r)return;fe.delete(e.reqId);try{r.reply(e)}catch{}}});const w=e=>{console.log(`child_exit sup=${Se} room=${r} pid=${g?.pid||"?"} code=${e||"?"}`),ce.delete(r),ue.delete(r),de.delete(r),le.delete(r),pe.delete(r),me.delete(r),he.delete(r);for(const[e,t]of fe)t?.askerRoom===r&&fe.delete(e)};if(g.on("exit",e=>w(e)),g.on("error",e=>{w();try{process.stderr.write(`\n ◇ ${r}: failed to launch its bridge (${e?.message||e}) — retrying next tick.\n`)}catch{}}),we)try{g.send({t:"update-available",version:we})}catch{}}if("1"!==process.env.TP_PAIRBUS_OFF)for(const e of r){if(!e?.code||!ce.has(e.code))continue;const r=Array.isArray(e.participants)?e.participants:[],t=2===r.length?r.find(e=>e&&e!==ee.user.id):null;if(!t){he.delete(e.code);continue}he.set(e.code,t);const o=E(ee.user.id,t);o&&sr(t,o)}Ue()};await ar();const dr=setInterval(ar,15e3);Le=!0,Be(),c();const lr=[];let pr=!1,ur=0,mr=0,fr=!1;const hr=setInterval(async()=>{if(pr)return;const e=Date.now();if(!claimLoopWedged({lastClaimOkAt:Xe,now:e}))return ur=0,void(mr=0);if(fr||e<mr)return;fr=!0;const r=b(ur);mr=e+r,ur+=1,console.log(`claim_watchdog sup=${Se} quiet_ms=${e-Xe} attempt=${ur} next_ms=${r} — reconnecting realtime + re-claiming`);try{Q.realtime?.connect?.()}catch{}ir=0;try{await Qe(),Ue()}catch{}finally{fr=!1}},2e4);hr.unref?.();const yr=setInterval(()=>{if(pr)return;const e=Date.now(),r=w({claimHeld:We,lastClaimOkAt:Xe,now:e});let t=!1;try{t=!!Ie.presenceState?.()[Re]}catch{t=!1}const o=_({tracking:r,subscribedAt:Math.max(Fe.subscribedAt,Xe),syncSeenAt:Fe.syncSeenAt,present:t,missingSince:Ye,now:e});if(Ye=o.missingSince,!Ye)return void(Ze=0);const n=o.verdict;if("recycle"===n){if(Ze===Ye)return;Ze=Ye,console.log(`presence_watchdog sup=${Se} action=recycle missing_ms=${e-Ye} track=${Ne} — full realtime disconnect+reconnect`);try{Q.realtime?.disconnect?.()}catch{}try{Q.realtime?.connect?.()}catch{}try{Ue()}catch{}}else"respawn"===n&&(console.log(`presence_watchdog sup=${Se} action=respawn missing_ms=${e-Ye} track=${Ne} — recycle failed, exiting for a clean supervisor respawn`),process.exit(1))},15e3);yr.unref?.();const vr=e=>{if(pr)return;pr=!0;const r=Ce?1:0;console.log(`supervisor_stop sup=${Se} reason=${e||"unknown"} restarting=${Ce} children=${ce.size}`),clearInterval(dr),clearInterval(hr),clearInterval(yr);for(const e of lr)try{clearInterval(e)}catch{}Ke.cancel(),z({pid:process.pid}),releaseSingleton();for(const r of ce.values())try{r.kill(e||"SIGTERM")}catch{}er.close();for(const{channel:e}of ye.values())try{Q.removeChannel(e)}catch{}(async()=>{try{We&&await Q.rpc("release_bridge",{p_bridge_id:xe})}catch{}try{await Ie.untrack()}catch{}try{await Q.removeChannel(Oe)}catch{}try{await Q.removeChannel(Ie)}catch{}process.exit(r)})(),setTimeout(()=>process.exit(r),1500)};for(const e of["SIGINT","SIGTERM","SIGHUP"])process.on(e,()=>vr(e));async function gr(){if(!_e&&v({pendingUpdate:we,stopping:pr,childIdle:de,childBetween:le,childPeer:pe,requested:ge})){if(_e=!0,!await function(e=2500){const r=[...ce.entries()];if(!r.length)return Promise.resolve(!0);const o=t();return new Promise(t=>{const n={pending:new Set(r.map(([e])=>e)),ok:!0,resolve:t,timer:null};n.timer=setTimeout(()=>{be.delete(o),t(!1)},e),n.timer.unref?.(),be.set(o,n);for(const[e,t]of r)try{t.send({t:"restart-probe",nonce:o})}catch{n.pending.delete(e),n.ok=!1}n.pending.size||(clearTimeout(n.timer),be.delete(o),t(n.ok))})}())return _e=!1,void process.stderr.write("\n ◆ bridge update still queued — an agent became active before the restart boundary.\n");process.stderr.write(`\n ◆ thinkpool-pair ${we} ready (running ${Y}) — restarting account bridge to update; sessions resume.\n`);try{const e=await import("./service.mjs");if(e.serviceActive(null)&&!1!==e.applyStagedServiceUpdate(null,we))return}catch{}if(_e=!1,Ce=!0,!se())try{e(process.execPath,[process.argv[1]],{detached:!0,stdio:"ignore",env:process.env}).unref()}catch{}vr("SIGTERM")}}if("1"===process.env.THINKPOOL_PAIR_AUTOUPDATE&&Y){const e=1e3*Math.max(60,parseInt(process.env.THINKPOOL_PAIR_UPDATE_INTERVAL,10)||1800),r=(e,r)=>{const t=e=>String(e).split("-")[0].split(".").map(e=>parseInt(e,10)||0),o=t(e),n=t(r);for(let e=0;e<3;e++){if((o[e]||0)>(n[e]||0))return!0;if((o[e]||0)<(n[e]||0))return!1}return!1},t=async()=>{const e=new AbortController,r=setTimeout(()=>e.abort(),8e3);try{const r=await fetch("https://registry.npmjs.org/thinkpool-pair/latest",{signal:e.signal,headers:{accept:"application/json"}});if(!r.ok)return null;const t=await r.json();return"string"==typeof t?.version?t.version:null}catch{return null}finally{clearTimeout(r)}},o=async()=>{const e=await t();if(e&&r(e,Y)){we||process.stderr.write(`\n ◆ thinkpool-pair ${e} published — "update ready" surfaced to each room; applies between turns.\n`);try{(await import("./service.mjs")).provisionRuntime(e)}catch(r){return void process.stderr.write(`\n ⚠ bridge ${e} could not be staged yet: ${r?.message||r}\n`)}we=e;for(const r of ce.values())try{r.send({t:"update-available",version:e})}catch{}gr()}};lr.push(setInterval(o,e).unref()),lr.push(setInterval(gr,15e3).unref()),lr.push(setTimeout(o,6e4).unref())}await new Promise(()=>{})}