thinkpool-pair 0.7.414 → 0.7.415

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/account.mjs CHANGED
@@ -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 g}from"./update-gate.mjs";import{makeThrottledTrack as v,presenceRecoveryEligible as w,presenceSelfEchoObservation as b,recoveryBackoffMs as _,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 q}from"./serve-dir.mjs";import{pairKeyFor as N,pairTopic as E,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{HostCapacityAuthority as L}from"./host-capacity.mjs";import{createPairBusBroker as H,mergePairRoomRoster as B,pairBusStatusFromRealtime as z,PAIR_BUS_STATUS as G}from"./pair-bus.mjs";import{readCloudSessionTranscript as K}from"./cloud-session-transcript.mjs";import{clearSupervisorReady as W,supervisorPresenceEchoed as J,writeSupervisorReady as V}from"./supervisor-ready.mjs";import{PAIR_CLI as X,pairCli as Y}from"./command-guidance.mjs";import{syncRealtimeAuth as Z}from"./design-edit.mjs";const Q=(()=>{try{return JSON.parse(n.readFileSync(new URL("./package.json",import.meta.url),"utf8")).version}catch{return null}})(),ee=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 re=o.join(i.homedir(),".thinkpool-pair","account.lock");function te(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=re){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(te(t))return!1;try{n.rmSync(e,{force:!0})}catch{return!1}}return!1}export function releaseSingleton(e=re){try{(parseInt(n.readFileSync(e,"utf8").trim(),10)||0)===process.pid&&n.rmSync(e,{force:!0})}catch{}}export function acquireMachineLock(e=re){return acquireSingleton(e)}export function releaseMachineLock(e=re){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 \`${Y("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: ${X}\n`),process.exit(0)),"expired"!==e?.status&&"not_found"!==e?.status||(process.stderr.write(`\n ◇ link expired — run \`${Y("login")}\` again.\n`),process.exit(1))}catch{}setTimeout(m,3e3)};m(),await new Promise(()=>{})}const oe=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 ne=o.join(i.homedir(),".thinkpool-pair","refresh.lock");export function acquireRefreshLock(e=ne,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=ne){try{n.rmSync(e,{force:!0})}catch{}}async function se(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=oe}=t,l=async r=>{const t=o();if(t?.refresh_token&&r&&t.refresh_token!==r){const r=await se(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 se(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=oe,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: ${Y("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 \`${X}\` (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 \`${Y("<ROOM>")}\`.\n`),process.exit(0)),W();const c=createStartupAnchor(),X=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`)});X||(releaseSingleton(),console.error(`\n ◇ Not linked to a thinkpool account on this machine.\n Link it: ${Y("login")}\n`),process.exit(0)),X.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:re,session:te}=X,oe=te.user?.email||"your account";let ne=te.access_token||null;const se=process.cwd();let ie=!1;try{const e=await import("./service.mjs");ie=e.serviceActive(null)}catch{}const ae=()=>"1"===process.env.THINKPOOL_PAIR_AUTOUPDATE||ie,ce="1"===process.env.THINKPOOL_PAIR_AUTOUPDATE?" (auto-updating background service — survives reboot, tracks @latest)\n":ie?" (background service — survives reboot, restarts on crash, pinned to this version)\n":` (run hands-off + auto-updating: ${Y("install-service")})\n`,de=f()||y()||se;process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${oe}\n New sessions auto-serve from ${de}${f()?" (your default)":y()?" (your most-recent Claude Code project)":""}\n (change it: ${Y("set-default-dir","<dir>")} · one room: ${Y("bind","<code>","<dir>")})\n${ce}`);const le=new Map,pe=new L,ue=new Map,me=new Map,fe=new Map,he=new Set,ye=new Map,ge=new Map,ve=new Map,we=new Map,be=new Map;let _e=!1,ke=null,$e=!1;const Re=new Map,Se=new Set,Te=new Map,Ie=i.hostname().replace(/\.local$/,""),Oe=(t?.()||"").slice(0,8);let xe=null;console.log(`◇ supervisor boot sup=${Oe} host=${Ie} pid=${process.pid} npx=${Q||"dev"}`),await Z(re,ne,s)||process.stderr.write("\n ⚠ realtime auth could not be initialized; private bridge control is unavailable.\n");const Ce=re.channel(`tpacct:${te.user.id}`,{config:{presence:{key:Ie},broadcast:{self:!1}}}),Ae=re.channel(`tpacct-control:${te.user.id}`,{config:{private:!0,broadcast:{self:!1}}}),Pe=t();let qe=!1,Ne=()=>{};Ae.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`),Ne()):process.stderr.write(`\n ◇ bind ${r}: directory not found (${t}) — ignored.\n`))}),Ae.on("broadcast",{event:"serve-room"},({payload:e})=>{const r=String(e?.code||"").toUpperCase().trim();/^[A-Z0-9]{4,12}$/.test(r)&&Ne()}),Ae.on("broadcast",{event:"agent-output-hygiene"},({payload:e})=>{if("boolean"==typeof e?.enabled)for(const r of le.values())try{r.send({t:"agent-output-hygiene",enabled:e.enabled})}catch{}}),Ae.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),ae())try{(await import("./service.mjs")).uninstallService(null)}catch{}process.kill(process.pid,"SIGTERM")});let Ee=!1;Ae.on("broadcast",{event:"restart"},async({payload:e}={})=>{const r=e?.nonce,t=async(e,t)=>{try{await Ae.send({type:"broadcast",event:"restart-status",payload:{nonce:r,ok:e,error:t}})}catch{}};if(Ee||$e)await t(!1,"a bridge update is already being prepared");else{Ee=!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");ke=n||("string"==typeof e?.version?e.version:Q)||"restart",_e=!0,await t(!0)}catch(e){return void await t(!1,`restart preparation failed: ${e?.message||e}`)}finally{Ee=!1}_r()}});let Me="init",Ue="connected";const De=v(Ce,{minMs:5e3,onStatus:e=>{Me=e}}),je=R(),Fe=()=>De({name:Ie,bridgeId:Pe,version:Q,rooms:[...le.keys()],readyRooms:[...he],refused:[...Te].map(([e,r])=>({code:e,reason:r})),service:ae(),providerPubKey:je,providers:S(),agents:D(U()),accountAuthState:Ue,ts:Date.now()}),Le=async e=>{try{await withTimeout(re.rpc("report_bridge_auth_state",{p_bridge_id:Pe,p_state:e}),5e3,"report_bridge_auth_state")}catch{}},He=(e,r)=>{try{Ae.send({type:"broadcast",event:e,payload:r})}catch{}};Ae.on("broadcast",{event:"master-brief-cleanup"},({payload:e})=>{const r=String(e?.nonce||""),t=String(e?.transcript||"").trim();if(!r||!t||t.length>12e3)return void He("master-brief-cleanup-res",{nonce:r,ok:!1,error:"That recording could not be cleaned up."});const o=[...he].find(e=>!1!==le.get(e)?.connected),n=o?le.get(o):null;if(n)try{n.send({t:"master-brief-cleanup",nonce:r,transcript:t,speaker:e?.speaker})}catch{He("master-brief-cleanup-res",{nonce:r,ok:!1,error:"The connected agent was unavailable."})}else He("master-brief-cleanup-res",{nonce:r,ok:!1,error:"Open a session with a connected agent to clean this Brief."})}),Ae.on("broadcast",{event:"provider-add"},async({payload:e})=>{const r=e?.nonce;let t;try{t=C(e?.sealed)}catch{return He("provider-add-res",{nonce:r,ok:!1,error:"could not decrypt the sealed payload"})}const o=I(t);o.ok&&(Fe(),process.stderr.write(`\n ◆ provider added (${String(t?.name||"").slice(0,40)}) — re-announced.\n`)),He("provider-add-res",{nonce:r,ok:o.ok,error:o.ok?void 0:o.error,id:o.ok?o.id:void 0})}),Ae.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&&(Fe(),process.stderr.write(`\n ◆ model added to an existing provider (${String(e?.model||"").slice(0,40)}) — re-announced.\n`)),He("provider-add-model-res",{nonce:r,ok:t.ok,error:t.ok?void 0:t.error,id:t.ok?t.id:void 0})}),Ae.on("broadcast",{event:"provider-remove"},async({payload:e})=>{const r=e?.nonce,t=x(e?.id);t.ok&&(Fe(),process.stderr.write("\n ◆ provider removed — re-announced.\n")),He("provider-remove-res",{nonce:r,ok:t.ok,error:t.ok?void 0:t.error})}),Ae.on("broadcast",{event:"providers-list-req"},async({payload:e})=>{const r=e?.nonce;He("providers-list-res",{nonce:r,ok:!0,providers:T()})}),Ae.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){Fe();for(const e of le.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`)}He("agent-visibility-res",{nonce:r,ok:o.ok,error:o.ok?void 0:o.error,agents:o.ok?o.agents:void 0})});let Be=k(),ze=!1,Ge=!1;const Ke=()=>{if(Ge||!ze)return;let e=!1;try{e=J(Ce.presenceState?.(),Ie,Pe,Q)}catch{e=!1}e&&(Ge=V({version:Q,bridgeId:Pe}),Ge||process.stderr.write("\n ◇ Could not record local supervisor readiness; the bridge remains running but installers will not claim it was confirmed.\n"))};Ce.on("presence",{event:"sync"},()=>{Be=k(Be,"sync"),Ke()});const We=await $(Ce,e=>{if("SUBSCRIBED"===e)Be=k(Be,e),Fe();else if("CLOSED"===e||"CHANNEL_ERROR"===e||"TIMED_OUT"===e){Be=k(Be,e);try{re.realtime?.connect?.()}catch{}}});We.connected||process.stderr.write(`\n ◇ account realtime not ready at startup (${We.status}) — staying alive and recovering in process.\n`);const Je=await $(Ae,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`)});Je.connected||process.stderr.write(`\n ◇ private account control not ready at startup (${Je.status}) — presence remains available.\n`);const Ve=scheduleTokenRefresh({sb:re,session:te,onRefreshed:async e=>{Ue="connected",a(e),Fe(),await Le("connected"),ne=e.access_token||ne;for(const e of le.values())try{e.send({t:"token",accessToken:ne})}catch{}},onReconnectRequired:async e=>{Ue="reconnect-required",d(e?.code||"refresh-token-invalid"),Fe(),await Le("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 Xe=!1,Ye=0,Ze=null,Qe=Date.now(),er=0,rr=0;const tr=async()=>{try{const{data:e}=await withTimeout(re.rpc("claim_bridge",{p_bridge_id:Pe,p_hostname:Ie}),1e4,"claim_bridge");Qe=Date.now();const r=Array.isArray(e)?e[0]:e,t=!!r?.held,o=r?.holder_host||"?";return t&&!Xe?process.stderr.write("\n ◆ holding this account — serving your sessions (single active bridge).\n"):!t&&Xe?process.stderr.write(`\n ◇ another device (${o}) took over this account — standing by.\n`):t||Ze===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`),Xe=t,Ye=t&&Number.isSafeInteger(Number(r?.fencing))&&Number(r.fencing)>0?Number(r.fencing):0,Ze=t?null:o,xe!==t&&(console.log(`claim_tick sup=${Oe} held=${t} holder=${t?Ie:o}`),xe=t),t}catch{return Xe}},or=H({channels:we,randomId:t}),nr=e=>or.resolve(e),sr=(e,r,t)=>or.requestFirst(e,r,t),ir=(e,r,t)=>or.requestCollect(e,r,t),ar=async e=>{try{const r=await withTimeout(re.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:[]}}},cr=(e,r)=>{if(!e||we.has(e))return;const o=E(r);if(!o)return;const n=re.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=[...le.keys()].filter(r=>ve.get(r)===e).map(e=>({code:e,name:ye.get(e)||null,host:Ie}));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})=>nr(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=le.get(s);if(!i||ve.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();ge.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{ge.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-peek-res"},({payload:e})=>nr(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=be.get(e);return(!t||r>=t.resetAt)&&(t={count:0,resetAt:r+6e4},be.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=le.get(s);if(!i||ve.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();ge.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{ge.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-post-res"},({payload:e})=>nr(e)).subscribe(e=>{s.status=z(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`)}),we.set(e,s)};let dr=0;const lr=async()=>{const e=Date.now();if(!(dr&&e-dr<6e4)){dr=e;try{await pr()}finally{dr===e&&(dr=0)}}};Ne=()=>{lr().catch(()=>{})};const pr=async()=>{if(!await tr()){if(le.size)for(const[,e]of le)try{e.kill("SIGTERM")}catch{}return void Fe()}let r=[],s=!1;const a=te.user.id;try{const{data:e,error:t}=await re.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[...Te.keys()])o.has(e)||Te.delete(e)}catch{}if(s)for(const e of P({rooms:r,childRooms:le.keys(),myUid:a})){const r=le.get(e);if(r){console.log(`child_retire sup=${Oe} 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||le.has(r)){r&&Te.delete(r);continue}if(!A({ownerId:s.owner_id,myUid:a,closedAt:s.closed_at}))continue;const l=q({bound:c[r],remembered:d[r],defaultDir:f()||y()||se,home:i.homedir()});if(l.refuse){if(Te.set(r,l.reason),!Se.has(r)){Se.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: ${Y("bind",r,"<repo-dir>")}\n`)}continue}const p=l.dir;if(!n.existsSync(p)){Te.set(r,"dir-gone"),Se.has(r)||(Se.add(r),process.stderr.write(`\n ◇ ${r}: bound directory is gone (${p}) — re-bind it.\n`));continue}Se.delete(r),Te.delete(r),l.fromDefault&&m(r,p),ye.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:Pe,TP_ACCOUNT_BRIDGE_FENCING:String(Ye)};delete u.THINKPOOL_PAIR_AUTOUPDATE,ne&&(u.TP_ACCESS_TOKEN=ne);const h=le.has(r),g=F(`spawn the room bridge for ${r}`);if(!g.ok){Se.has(`memory:${r}`)||(Se.add(`memory:${r}`),process.stderr.write(`\n ◇ ${r}: ${g.reason}\n`)),Te.set(r,"host-memory");continue}Se.delete(`memory:${r}`);const v=e(process.execPath,[ee,r,"--headless","--auto=claude"],{cwd:p,stdio:["inherit","inherit","inherit","ipc"],env:u});console.log(`child_spawn sup=${Oe} room=${r} pid=${v.pid} dir=${p} had_prior=${h}`),le.set(r,v),he.delete(r),ue.set(r,!1),me.set(r,!1),fe.set(r,!1),v.on("message",e=>{if(e)if("room-ready"===e.t)he.add(r),console.log(`child_ready sup=${Oe} room=${r} pid=${v.pid}`),Fe();else if("idle"===e.t)ue.set(r,!!e.idle),me.set(r,null!=e.between?!!e.between:!!e.idle),fe.set(r,!!e.webPeer),_r();else if("busy"===e.t)ue.set(r,!1),me.set(r,!1);else if("master-brief-cleanup-res"===e.t)He("master-brief-cleanup-res",{nonce:e.nonce,ok:!0===e.ok,...e.brief?{brief:e.brief}:{},...e.error?{error:e.error}:{}});else if("capacity-acquire"===e.t){const t=pe.acquire({room:r,terminalId:e.terminalId});try{v.send({t:"capacity-acquire-result",reqId:e.reqId,...t})}catch{t.ok&&pe.release({room:r,terminalId:e.terminalId})}}else if("capacity-release"===e.t){if(pe.release({room:r,terminalId:e.terminalId}))for(const e of le.values())try{e.send({t:"capacity-changed"})}catch{}}else if("restart-probe-result"===e.t){const t=Re.get(e.nonce);if(!t||!t.pending.has(r)||le.get(r)!==v)return;t.pending.delete(r),t.ok=t.ok&&!0===e.between,me.set(r,!0===e.between),t.pending.size||(clearTimeout(t.timer),Re.delete(e.nonce),t.resolve(t.ok))}else if("apply-update"===e.t)_e=!0,_r();else if("pair-list-req"===e.t)(async()=>{if("1"===process.env.TP_CROSSROOM_OFF){try{v.send({t:"pair-list-res",reqId:e.reqId,rooms:[],error:"Cross-session reach is turned off on this bridge."})}catch{}return}const t=[...le.keys()].filter(e=>e!==r).map(e=>({code:e,name:ye.get(e)||null}));let o=[],n=[];if("1"!==process.env.TP_PAIRBUS_OFF){const e=await Promise.all([ar(r),we.size?ir("peer-list-req",{},1200):Promise.resolve([])]);o=e[0].rooms,n=e[1]}const s=B({authorized:o,replies:n,myUid:te.user.id,localCodes:le.keys()});try{v.send({t:"pair-list-res",reqId:e.reqId,rooms:[...t,...s]})}catch{}})();else if("pair-peek-req"===e.t)(async()=>{const o=r=>{try{v.send({t:"pair-peek-res",reqId:e.reqId,...r})}catch{}},n=async(r,t)=>{if(!t.verified)return o({error:`Room ${r}'s pair authorization could not be verified, so its cloud transcript was not read.`});try{const t=await withTimeout(K({sb:re,targetRoom:r,terminal:e.terminal,lines:e.lines}),5e3,"paired cloud transcript");return o({text:t})}catch(e){return o({error:`Room ${r}'s host bridge is offline, and its cloud transcript could not be read: ${e?.message||e}`})}};if("1"===process.env.TP_CROSSROOM_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const s=String(e.targetRoom||"").toUpperCase().trim();if(!s||s===r)return o({error:`${s||"That"} is not another of your active sessions.`});const i=le.get(s);if(i){const n=t();ge.set(n,{reply:e=>o({text:e.text,error:e.error}),askerRoom:r});try{i.send({t:"pair-peek-local",reqId:n,terminal:e.terminal,lines:e.lines})}catch{ge.delete(n),o({error:`Could not reach session ${s}.`})}return}if("1"===process.env.TP_PAIRBUS_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const[a,c]=await Promise.all([ar(r),we.size?ir("peer-list-req",{},800):Promise.resolve([])]);if(a.verified&&!a.rooms.some(e=>e?.code===s&&e?.owner_id!==te.user.id))return o({error:`Room ${s} is not shared with this pair.`});if(!we.size)return n(s,a);if(!c.some(e=>e?.rooms?.some(e=>e?.code===s)))return n(s,a);const d=await(l=s,p=e.terminal,u=e.lines,sr("peer-peek-req",{targetRoom:l,terminal:p,lines:u},M.busTimeoutMs));var l,p,u;if(!d)return n(s,a);o({text:d.text,error:d.error})})();else if("pair-post-req"===e.t)(async()=>{const o=r=>{try{v.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=le.get(n);if(s){const i=t();ge.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:Ie,fromTerminalName:e.fromTerminalName,text:e.text,terminal:e.terminal})}catch{ge.delete(i),o({error:`Could not reach session ${n}.`})}return}const[i,a]=await Promise.all([ar(r),we.size?ir("peer-list-req",{},1200):Promise.resolve([])]);if(i.verified&&!i.rooms.some(e=>e?.code===n&&e?.owner_id!==te.user.id))return o({error:`Room ${n} is not shared with this pair.`});if(!we.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,sr("peer-post-req",{targetRoom:d,terminal:l,text:p,fromRoom:u,fromHost:Ie,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=ge.get(e.reqId);if(!r)return;ge.delete(e.reqId);try{r.reply(e)}catch{}}});const w=e=>{if(console.log(`child_exit sup=${Oe} room=${r} pid=${v?.pid||"?"} code=${e||"?"}`),le.delete(r),he.delete(r),ue.delete(r),me.delete(r),fe.delete(r),ye.delete(r),ve.delete(r),pe.releaseRoom(r))for(const e of le.values())try{e.send({t:"capacity-changed"})}catch{}for(const[e,t]of ge)t?.askerRoom===r&&ge.delete(e)};if(v.on("exit",e=>w(e)),v.on("error",e=>{w();try{process.stderr.write(`\n ◇ ${r}: failed to launch its bridge (${e?.message||e}) — retrying next tick.\n`)}catch{}}),ke)try{v.send({t:"update-available",version:ke})}catch{}}if("1"!==process.env.TP_PAIRBUS_OFF)for(const e of r){if(!e?.code||!le.has(e.code))continue;const r=Array.isArray(e.participants)?e.participants:[],t=2===r.length?r.find(e=>e&&e!==te.user.id):null;if(!t){ve.delete(e.code);continue}ve.set(e.code,t);const o=N(te.user.id,t);o&&cr(t,o)}Fe()};await lr();const ur=setInterval(lr,15e3);ze=!0,Ke(),c();const mr=[];let fr=!1,hr=0,yr=0,gr=!1;const vr=setInterval(async()=>{if(fr)return;const e=Date.now();if(!claimLoopWedged({lastClaimOkAt:Qe,now:e}))return hr=0,void(yr=0);if(gr||e<yr)return;gr=!0;const r=_(hr);yr=e+r,hr+=1,console.log(`claim_watchdog sup=${Oe} quiet_ms=${e-Qe} attempt=${hr} next_ms=${r} — reconnecting realtime + re-claiming`);try{re.realtime?.connect?.()}catch{}dr=0;try{await tr(),Fe()}catch{}finally{gr=!1}},2e4);vr.unref?.();const wr=setInterval(()=>{if(fr)return;const e=Date.now(),r=w({claimHeld:Xe,lastClaimOkAt:Qe,now:e});let t=!1;try{t=!!Ce.presenceState?.()[Ie]}catch{t=!1}const o=b({tracking:r,subscribedAt:Math.max(Be.subscribedAt,Qe),syncSeenAt:Be.syncSeenAt,present:t,missingSince:er,now:e});if(er=o.missingSince,!er)return void(rr=0);const n=o.verdict;if("recycle"===n){if(rr===er)return;rr=er,console.log(`presence_watchdog sup=${Oe} action=recycle missing_ms=${e-er} track=${Me} — full realtime disconnect+reconnect`);try{re.realtime?.disconnect?.()}catch{}try{re.realtime?.connect?.()}catch{}try{Fe()}catch{}}else"respawn"===n&&(console.log(`presence_watchdog sup=${Oe} action=respawn missing_ms=${e-er} track=${Me} — recycle failed, exiting for a clean supervisor respawn`),process.exit(1))},15e3);wr.unref?.();const br=e=>{if(fr)return;fr=!0;const r=qe?1:0;console.log(`supervisor_stop sup=${Oe} reason=${e||"unknown"} restarting=${qe} children=${le.size}`),clearInterval(ur),clearInterval(vr),clearInterval(wr);for(const e of mr)try{clearInterval(e)}catch{}Ve.cancel(),W({pid:process.pid}),releaseSingleton();for(const r of le.values())try{r.kill(e||"SIGTERM")}catch{}or.close();for(const{channel:e}of we.values())try{re.removeChannel(e)}catch{}(async()=>{try{Xe&&await re.rpc("release_bridge",{p_bridge_id:Pe})}catch{}try{await Ce.untrack()}catch{}try{await re.removeChannel(Ae)}catch{}try{await re.removeChannel(Ce)}catch{}process.exit(r)})(),setTimeout(()=>process.exit(r),1500)};for(const e of["SIGINT","SIGTERM","SIGHUP"])process.on(e,()=>br(e));async function _r(){if(!$e&&g({pendingUpdate:ke,stopping:fr,childIdle:ue,childBetween:me,childPeer:fe,requested:_e})){if($e=!0,!await function(e=2500){const r=[...le.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(()=>{Re.delete(o),t(!1)},e),n.timer.unref?.(),Re.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),Re.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 ${ke} ready (running ${Q}) — restarting account bridge to update; sessions resume.\n`);try{const e=await import("./service.mjs");if(e.serviceActive(null)&&!1!==e.applyStagedServiceUpdate(null,ke))return}catch{}if($e=!1,qe=!0,!ae())try{e(process.execPath,[process.argv[1]],{detached:!0,stdio:"ignore",env:process.env}).unref()}catch{}br("SIGTERM")}}if("1"===process.env.THINKPOOL_PAIR_AUTOUPDATE&&Q){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,Q)){ke||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`)}ke=e;for(const r of le.values())try{r.send({t:"update-available",version:e})}catch{}_r()}};mr.push(setInterval(o,e).unref()),mr.push(setInterval(_r,15e3).unref()),mr.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 g}from"./update-gate.mjs";import{makeThrottledTrack as v,presenceRecoveryEligible as w,presenceSelfEchoObservation as b,recoveryBackoffMs as k,reducePresenceChannelEvidence as _,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 q}from"./serve-dir.mjs";import{pairKeyFor as N,pairTopic as E,CROSSROOM_BUS as M}from"./cross-terminal.mjs";import{installedAgentCommands as U}from"./agent-detect.mjs";import{agentVisibilityStates as L,setAgentPickerVisibility as D}from"./agent-visibility.mjs";import{hostMemoryAdmission as j}from"./host-memory.mjs";import{HostCapacityAuthority as F}from"./host-capacity.mjs";import{createPairBusBroker as H,mergePairRoomRoster as B,pairBusStatusFromRealtime as G,PAIR_BUS_STATUS as z}from"./pair-bus.mjs";import{readCloudSessionTranscript as K}from"./cloud-session-transcript.mjs";import{clearSupervisorReady as W,supervisorPresenceEchoed as J,writeSupervisorReady as V}from"./supervisor-ready.mjs";import{PAIR_CLI as X,pairCli as Y}from"./command-guidance.mjs";import{syncRealtimeAuth as Z}from"./design-edit.mjs";import{crossRotationLock as Q}from"./rotation-lock.mjs";import{loadOrCreateBridgeId as ee,bridgeLabel as re}from"./bridge-identity.mjs";const te=(()=>{try{return JSON.parse(n.readFileSync(new URL("./package.json",import.meta.url),"utf8")).version}catch{return null}})(),oe=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 ne=o.join(i.homedir(),".thinkpool-pair","account.lock");function se(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=ne){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(se(t))return!1;try{n.rmSync(e,{force:!0})}catch{return!1}}return!1}export function releaseSingleton(e=ne){try{(parseInt(n.readFileSync(e,"utf8").trim(),10)||0)===process.pid&&n.rmSync(e,{force:!0})}catch{}}export function acquireMachineLock(e=ne){return acquireSingleton(e)}export function releaseMachineLock(e=ne){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 \`${Y("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: ${X}\n`),process.exit(0)),"expired"!==e?.status&&"not_found"!==e?.status||(process.stderr.write(`\n ◇ link expired — run \`${Y("login")}\` again.\n`),process.exit(1))}catch{}setTimeout(m,3e3)};m(),await new Promise(()=>{})}const ie=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 ae=o.join(i.homedir(),".thinkpool-pair","refresh.lock");export function acquireRefreshLock(e=ae,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=ae){try{n.rmSync(e,{force:!0})}catch{}}async function ce(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=ie,crossLock:l=null}=t,p=async r=>{const t=o();if(t?.refresh_token&&r&&t.refresh_token!==r){const r=await ce(e,t);if(r)return{session:r,adopted:!0}}return null};let u=await p(r);if(u)return u;const m=o()?.refresh_token||r;if(!m)return null;let f=!1;const h=async()=>{if(l&&!f){let e=!0;try{e=await l.acquire()}catch{e=!0}if(!1===e)return!1;f=!0}return s()};let y=await h();if(!y){for(let e=0;e<20;e++){if(await d(250),u=await p(m),u)return f&&await(l?.release?.()),f=!1,u;if(await h()){y=!0;break}}if(!y)return f&&await(l?.release?.()),f=!1,null}try{if(u=await p(m),u)return u;const r=o()?.refresh_token||m,{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{if(f){f=!1;try{await(l?.release?.())}catch{}}y&&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,{crossLockFor:t}={}){const o=c();if(!o?.refresh_token)return null;if(o.reconnect_required)return{reconnectRequired:!0};const n=s(e,r,{auth:{persistSession:!1,autoRefreshToken:!1}});if(savedTokenStillValid(o)){const e=await ce(n,o);if(e)return{sb:n,session:e}}const i=await refreshAccountSession(n,o.refresh_token,t?{crossLock:t(n)}:{});return i?.session?{sb:n,session:i.session}:accountRefreshNeedsReconnect(i?.error)?(d(i.error.code),{reconnectRequired:!0}):null}export async function authedClientWithRetry(e,r,t={}){const{attempt:o=authedClient,load:n=c,wait:s=ie,onRetry:i=null,maxAttempts:a=1/0,baseMs:d=15e3,capMs:l=3e5,crossLockFor:p=null}=t;let u=d;for(let t=1;t<=a;t++){const c=await o(e,r,{crossLockFor:p});if(c?.reconnectRequired)return c;if(c)return c;if(!n()?.refresh_token)return null;if(t>=a)break;i?.(u,t),await s(u),u=Math.min(2*u,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,crossLock:c=null}){let d=null,l=r,p=!1,u=!1;const m=e=>{p||(clearTimeout(d),d=setTimeout(h,Math.max(1e3,e)),d?.unref&&d.unref())},f=()=>{if(s)return s;const e=l?.expires_at?1e3*l.expires_at:Date.now()+36e5;return Math.max(3e4,e-Date.now()-n)};async function h(){if(!p)try{const r=await a(e,l.refresh_token,c?{crossLock:c}:void 0);if(!r?.session){if(accountRefreshNeedsReconnect(r?.error)&&!u){u=!0;try{await(o?.(r.error))}catch{}}throw r?.error||new Error("no session")}l=r.session;try{e.realtime?.setAuth?.(l.access_token)}catch{}u=!1;try{await(t?.(l))}catch{}m(f())}catch{m(i)}}return m(f()),{cancel:()=>{p=!0,clearTimeout(d)},_tick:h}}export function runBind(e,r){const t=(e||"").toUpperCase().trim();t||(console.error(`usage: ${Y("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 \`${X}\` (or restart it) to serve this session.`),process.exit(0)}export function demoteChildWithGrace({room:e,child:r,handoffInFlight:t,refreshClaim:o,waitMs:n=4e4}){if(t.has(e))return!1;try{r.send({t:"handoff-out"})}catch{}let s=!1;return new Promise(r=>{const o=setTimeout(()=>{s||(s=!0,t.delete(e),r(null))},n);t.set(e,{resolve:()=>{s||(s=!0,clearTimeout(o),t.delete(e),r(e))},timer:o})}).then(async()=>{try{if(await o())return}catch{}try{r.kill("SIGTERM")}catch{}}).catch(()=>{}),!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 \`${Y("<ROOM>")}\`.\n`),process.exit(0)),W();const c=createStartupAnchor(),X=ee(),ne=t(),se=e=>Q(e,X),ie=await authedClientWithRetry(r,s,{crossLockFor:se,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`)});ie||(releaseSingleton(),console.error(`\n ◇ Not linked to a thinkpool account on this machine.\n Link it: ${Y("login")}\n`),process.exit(0)),ie.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:ae,session:ce}=ie,de=ce.user?.email||"your account";let le=ce.access_token||null;const pe=process.cwd();let ue=!1;try{const e=await import("./service.mjs");ue=e.serviceActive(null)}catch{}const me=()=>"1"===process.env.THINKPOOL_PAIR_AUTOUPDATE||ue,fe="1"===process.env.THINKPOOL_PAIR_AUTOUPDATE?" (auto-updating background service — survives reboot, tracks @latest)\n":ue?" (background service — survives reboot, restarts on crash, pinned to this version)\n":` (run hands-off + auto-updating: ${Y("install-service")})\n`,he=f()||y()||pe;process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${de}\n New sessions auto-serve from ${he}${f()?" (your default)":y()?" (your most-recent Claude Code project)":""}\n (change it: ${Y("set-default-dir","<dir>")} · one room: ${Y("bind","<code>","<dir>")})\n${fe}`);const ye=new Map,ge=new F,ve=new Map,we=new Map,be=new Map,ke=new Set,_e=new Map,$e=new Map,Re=new Map,Se=new Map,Te=new Map,Ie=new Map;let Oe=!1,xe=null,Ce=!1;const Ae=new Map,Pe=new Set,qe=new Map,Ne=i.hostname().replace(/\.local$/,""),Ee=(t?.()||"").slice(0,8);let Me=null;console.log(`◇ supervisor boot sup=${Ee} host=${Ne} pid=${process.pid} npx=${te||"dev"}`),await Z(ae,le,s)||process.stderr.write("\n ⚠ realtime auth could not be initialized; private bridge control is unavailable.\n");const Ue=ae.channel(`tpacct:${ce.user.id}`,{config:{presence:{key:Ne},broadcast:{self:!1}}}),Le=ae.channel(`tpacct-control:${ce.user.id}`,{config:{private:!0,broadcast:{self:!1}}});let De=!1,je=()=>{};Le.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`),je()):process.stderr.write(`\n ◇ bind ${r}: directory not found (${t}) — ignored.\n`))}),Le.on("broadcast",{event:"serve-room"},({payload:e})=>{const r=String(e?.code||"").toUpperCase().trim();/^[A-Z0-9]{4,12}$/.test(r)&&je()}),Le.on("broadcast",{event:"agent-output-hygiene"},({payload:e})=>{if("boolean"==typeof e?.enabled)for(const r of ye.values())try{r.send({t:"agent-output-hygiene",enabled:e.enabled})}catch{}}),Le.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),me())try{(await import("./service.mjs")).uninstallService(null)}catch{}process.kill(process.pid,"SIGTERM")});let Fe=!1;Le.on("broadcast",{event:"restart"},async({payload:e}={})=>{const r=e?.nonce,t=async(e,t)=>{try{await Le.send({type:"broadcast",event:"restart-status",payload:{nonce:r,ok:e,error:t}})}catch{}};if(Fe||Ce)await t(!1,"a bridge update is already being prepared");else{Fe=!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");xe=n||("string"==typeof e?.version?e.version:te)||"restart",Oe=!0,await t(!0)}catch(e){return void await t(!1,`restart preparation failed: ${e?.message||e}`)}finally{Fe=!1}Or()}});let He="init",Be="connected";const Ge=v(Ue,{minMs:5e3,onStatus:e=>{He=e}}),ze=R(),Ke=()=>Ge({name:Ne,bridgeId:ne,version:te,rooms:[...ye.keys()],readyRooms:[...ke],refused:[...qe].map(([e,r])=>({code:e,reason:r})),service:me(),providerPubKey:ze,providers:S(),agents:L(U()),accountAuthState:Be,ts:Date.now()}),We=async e=>{try{await withTimeout(ae.rpc("report_bridge_auth_state",{p_bridge_id:X,p_state:e}),5e3,"report_bridge_auth_state")}catch{}},Je=(e,r)=>{try{Le.send({type:"broadcast",event:e,payload:r})}catch{}};Le.on("broadcast",{event:"master-brief-cleanup"},({payload:e})=>{const r=String(e?.nonce||""),t=String(e?.transcript||"").trim();if(!r||!t||t.length>12e3)return void Je("master-brief-cleanup-res",{nonce:r,ok:!1,error:"That recording could not be cleaned up."});const o=[...ke].find(e=>!1!==ye.get(e)?.connected),n=o?ye.get(o):null;if(n)try{n.send({t:"master-brief-cleanup",nonce:r,transcript:t,speaker:e?.speaker})}catch{Je("master-brief-cleanup-res",{nonce:r,ok:!1,error:"The connected agent was unavailable."})}else Je("master-brief-cleanup-res",{nonce:r,ok:!1,error:"Open a session with a connected agent to clean this Brief."})}),Le.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&&(Ke(),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})}),Le.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&&(Ke(),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})}),Le.on("broadcast",{event:"provider-remove"},async({payload:e})=>{const r=e?.nonce,t=x(e?.id);t.ok&&(Ke(),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})}),Le.on("broadcast",{event:"providers-list-req"},async({payload:e})=>{const r=e?.nonce;Je("providers-list-res",{nonce:r,ok:!0,providers:T()})}),Le.on("broadcast",{event:"agent-visibility-set"},async({payload:e})=>{const r=e?.nonce,t=U(),o=D(e?.cmd,!0===e?.enabled,{installed:t});if(o.ok){Ke();for(const e of ye.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 Ve=_(),Xe=!1,Ye=!1;const Ze=()=>{if(Ye||!Xe)return;let e=!1;try{e=J(Ue.presenceState?.(),Ne,ne,te)}catch{e=!1}e&&(Ye=V({version:te,bridgeId:ne}),Ye||process.stderr.write("\n ◇ Could not record local supervisor readiness; the bridge remains running but installers will not claim it was confirmed.\n"))};Ue.on("presence",{event:"sync"},()=>{Ve=_(Ve,"sync"),Ze()});const Qe=await $(Ue,e=>{if("SUBSCRIBED"===e)Ve=_(Ve,e),Ke();else if("CLOSED"===e||"CHANNEL_ERROR"===e||"TIMED_OUT"===e){Ve=_(Ve,e);try{ae.realtime?.connect?.()}catch{}}});Qe.connected||process.stderr.write(`\n ◇ account realtime not ready at startup (${Qe.status}) — staying alive and recovering in process.\n`);const er=await $(Le,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`)});er.connected||process.stderr.write(`\n ◇ private account control not ready at startup (${er.status}) — presence remains available.\n`);const rr=scheduleTokenRefresh({sb:ae,session:ce,crossLock:se(ae),onRefreshed:async e=>{Be="connected",a(e),Ke(),await We("connected"),le=e.access_token||le;for(const e of ye.values())try{e.send({t:"token",accessToken:le})}catch{}},onReconnectRequired:async e=>{Be="reconnect-required",d(e?.code||"refresh-token-invalid"),Ke(),await We("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 tr=!1,or=0,nr=null,sr=Date.now(),ir=0,ar=0,cr="";const dr=async()=>{try{const{data:e}=await withTimeout(ae.rpc("claim_bridge",{p_bridge_id:X,p_hostname:Ne}),1e4,"claim_bridge");sr=Date.now();const r=Array.isArray(e)?e[0]:e,t=!!r?.held,o=r?.holder_host||"?";return t&&!tr?process.stderr.write("\n ◆ holding this account — serving your sessions (single active bridge).\n"):!t&&tr?process.stderr.write(`\n ◇ another device (${o}) took over this account — standing by.\n`):t||nr===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`),tr=t,or=t&&Number.isSafeInteger(Number(r?.fencing))&&Number(r.fencing)>0?Number(r.fencing):0,nr=t?null:o,Me!==t&&(console.log(`claim_tick sup=${Ee} held=${t} holder=${t?Ne:o}`),Me=t),t}catch{return tr}},lr=H({channels:Te,randomId:t}),pr=e=>lr.resolve(e),ur=(e,r,t)=>lr.requestFirst(e,r,t),mr=(e,r,t)=>lr.requestCollect(e,r,t),fr=async e=>{try{const r=await withTimeout(ae.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:[]}}},hr=(e,r)=>{if(!e||Te.has(e))return;const o=E(r);if(!o)return;const n=ae.channel(o,{config:{private:!0,broadcast:{self:!1}}}),s={channel:n,key:r,status:z.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=[...ye.keys()].filter(r=>Se.get(r)===e).map(e=>({code:e,name:$e.get(e)||null,host:Ne}));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})=>pr(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=ye.get(s);if(!i||Se.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();Re.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{Re.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-peek-res"},({payload:e})=>pr(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=Ie.get(e);return(!t||r>=t.resetAt)&&(t={count:0,resetAt:r+6e4},Ie.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=ye.get(s);if(!i||Se.get(s)!==e)return n({error:`Room ${s} is not a room this device shares with you.`});const a=t();Re.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{Re.delete(a),n({error:`Could not reach room ${s}.`})}})(e,n,r)).on("broadcast",{event:"peer-post-res"},({payload:e})=>pr(e)).subscribe(e=>{s.status=G(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`)}),Te.set(e,s)};let yr=0;const gr=async()=>{const e=Date.now();if(!(yr&&e-yr<6e4)){yr=e;try{await vr()}finally{yr===e&&(yr=0)}}};je=()=>{gr().catch(()=>{})};const vr=async()=>{if(await(async()=>{try{const{data:e}=await withTimeout(ae.rpc("enroll_bridge",{p_bridge_id:X,p_label:re(),p_version:te}),1e4,"enroll_bridge"),r=Array.isArray(e)?e[0]:e,t=r?.reason||"";t!==cr&&("revoked"===t?process.stderr.write("\n ◇ this device was revoked from your thinkpool account — standing by.\n Restore it from the account page to serve again.\n"):"foreign"===t&&process.stderr.write("\n ◇ this machine's bridge identity belongs to a different thinkpool account.\n Remove ~/.thinkpool-pair/bridge-id and relink to enroll fresh.\n"),cr=t)}catch{}})(),!await dr()){if(ye.size)for(const[e,r]of ye)demoteChildWithGrace({room:e,child:r,handoffInFlight:_e,refreshClaim:dr});return void Ke()}let r=[],s=!1;const a=ce.user.id;try{const{data:e,error:t}=await ae.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[...qe.keys()])o.has(e)||qe.delete(e)}catch{}if(s)for(const e of P({rooms:r,childRooms:ye.keys(),myUid:a})){const r=ye.get(e);if(r){console.log(`child_retire sup=${Ee} 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||ye.has(r)){r&&qe.delete(r);continue}if(!A({ownerId:s.owner_id,myUid:a,closedAt:s.closed_at}))continue;const l=q({bound:c[r],remembered:d[r],defaultDir:f()||y()||pe,home:i.homedir()});if(l.refuse){if(qe.set(r,l.reason),!Pe.has(r)){Pe.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: ${Y("bind",r,"<repo-dir>")}\n`)}continue}const p=l.dir;if(!n.existsSync(p)){qe.set(r,"dir-gone"),Pe.has(r)||(Pe.add(r),process.stderr.write(`\n ◇ ${r}: bound directory is gone (${p}) — re-bind it.\n`));continue}Pe.delete(r),qe.delete(r),l.fromDefault&&m(r,p),$e.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:X,TP_ACCOUNT_BRIDGE_FENCING:String(or)};delete u.THINKPOOL_PAIR_AUTOUPDATE,le&&(u.TP_ACCESS_TOKEN=le);const h=ye.has(r),g=j(`spawn the room bridge for ${r}`);if(!g.ok){Pe.has(`memory:${r}`)||(Pe.add(`memory:${r}`),process.stderr.write(`\n ◇ ${r}: ${g.reason}\n`)),qe.set(r,"host-memory");continue}Pe.delete(`memory:${r}`);const v=e(process.execPath,[oe,r,"--headless","--auto=claude"],{cwd:p,stdio:["inherit","inherit","inherit","ipc"],env:u});console.log(`child_spawn sup=${Ee} room=${r} pid=${v.pid} dir=${p} had_prior=${h}`),ye.set(r,v),ke.delete(r),ve.set(r,!1),we.set(r,!1),be.set(r,!1),v.on("message",e=>{if(e)if("room-ready"===e.t)ke.add(r),console.log(`child_ready sup=${Ee} room=${r} pid=${v.pid}`),Ke();else if("idle"===e.t)ve.set(r,!!e.idle),we.set(r,null!=e.between?!!e.between:!!e.idle),be.set(r,!!e.webPeer),Or();else if("busy"===e.t)ve.set(r,!1),we.set(r,!1);else if("handoff-done"===e.t){const e=_e.get(r);if(e&&e.resolve)try{e.resolve()}catch{}}else if("master-brief-cleanup-res"===e.t)Je("master-brief-cleanup-res",{nonce:e.nonce,ok:!0===e.ok,...e.brief?{brief:e.brief}:{},...e.error?{error:e.error}:{}});else if("capacity-acquire"===e.t){const t=ge.acquire({room:r,terminalId:e.terminalId});try{v.send({t:"capacity-acquire-result",reqId:e.reqId,...t})}catch{t.ok&&ge.release({room:r,terminalId:e.terminalId})}}else if("capacity-release"===e.t){if(ge.release({room:r,terminalId:e.terminalId}))for(const e of ye.values())try{e.send({t:"capacity-changed"})}catch{}}else if("restart-probe-result"===e.t){const t=Ae.get(e.nonce);if(!t||!t.pending.has(r)||ye.get(r)!==v)return;t.pending.delete(r),t.ok=t.ok&&!0===e.between,we.set(r,!0===e.between),t.pending.size||(clearTimeout(t.timer),Ae.delete(e.nonce),t.resolve(t.ok))}else if("apply-update"===e.t)Oe=!0,Or();else if("pair-list-req"===e.t)(async()=>{if("1"===process.env.TP_CROSSROOM_OFF){try{v.send({t:"pair-list-res",reqId:e.reqId,rooms:[],error:"Cross-session reach is turned off on this bridge."})}catch{}return}const t=[...ye.keys()].filter(e=>e!==r).map(e=>({code:e,name:$e.get(e)||null}));let o=[],n=[];if("1"!==process.env.TP_PAIRBUS_OFF){const e=await Promise.all([fr(r),Te.size?mr("peer-list-req",{},1200):Promise.resolve([])]);o=e[0].rooms,n=e[1]}const s=B({authorized:o,replies:n,myUid:ce.user.id,localCodes:ye.keys()});try{v.send({t:"pair-list-res",reqId:e.reqId,rooms:[...t,...s]})}catch{}})();else if("pair-peek-req"===e.t)(async()=>{const o=r=>{try{v.send({t:"pair-peek-res",reqId:e.reqId,...r})}catch{}},n=async(r,t)=>{if(!t.verified)return o({error:`Room ${r}'s pair authorization could not be verified, so its cloud transcript was not read.`});try{const t=await withTimeout(K({sb:ae,targetRoom:r,terminal:e.terminal,lines:e.lines}),5e3,"paired cloud transcript");return o({text:t})}catch(e){return o({error:`Room ${r}'s host bridge is offline, and its cloud transcript could not be read: ${e?.message||e}`})}};if("1"===process.env.TP_CROSSROOM_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const s=String(e.targetRoom||"").toUpperCase().trim();if(!s||s===r)return o({error:`${s||"That"} is not another of your active sessions.`});const i=ye.get(s);if(i){const n=t();Re.set(n,{reply:e=>o({text:e.text,error:e.error}),askerRoom:r});try{i.send({t:"pair-peek-local",reqId:n,terminal:e.terminal,lines:e.lines})}catch{Re.delete(n),o({error:`Could not reach session ${s}.`})}return}if("1"===process.env.TP_PAIRBUS_OFF)return o({error:"Cross-session reach is turned off on this bridge."});const[a,c]=await Promise.all([fr(r),Te.size?mr("peer-list-req",{},800):Promise.resolve([])]);if(a.verified&&!a.rooms.some(e=>e?.code===s&&e?.owner_id!==ce.user.id))return o({error:`Room ${s} is not shared with this pair.`});if(!Te.size)return n(s,a);if(!c.some(e=>e?.rooms?.some(e=>e?.code===s)))return n(s,a);const d=await(l=s,p=e.terminal,u=e.lines,ur("peer-peek-req",{targetRoom:l,terminal:p,lines:u},M.busTimeoutMs));var l,p,u;if(!d)return n(s,a);o({text:d.text,error:d.error})})();else if("pair-post-req"===e.t)(async()=>{const o=r=>{try{v.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=ye.get(n);if(s){const i=t();Re.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:Ne,fromTerminalName:e.fromTerminalName,text:e.text,terminal:e.terminal})}catch{Re.delete(i),o({error:`Could not reach session ${n}.`})}return}const[i,a]=await Promise.all([fr(r),Te.size?mr("peer-list-req",{},1200):Promise.resolve([])]);if(i.verified&&!i.rooms.some(e=>e?.code===n&&e?.owner_id!==ce.user.id))return o({error:`Room ${n} is not shared with this pair.`});if(!Te.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,ur("peer-post-req",{targetRoom:d,terminal:l,text:p,fromRoom:u,fromHost:Ne,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=Re.get(e.reqId);if(!r)return;Re.delete(e.reqId);try{r.reply(e)}catch{}}});const w=e=>{if(console.log(`child_exit sup=${Ee} room=${r} pid=${v?.pid||"?"} code=${e||"?"}`),ye.delete(r),ke.delete(r),ve.delete(r),we.delete(r),be.delete(r),$e.delete(r),Se.delete(r),ge.releaseRoom(r))for(const e of ye.values())try{e.send({t:"capacity-changed"})}catch{}for(const[e,t]of Re)t?.askerRoom===r&&Re.delete(e)};if(v.on("exit",e=>w(e)),v.on("error",e=>{w();try{process.stderr.write(`\n ◇ ${r}: failed to launch its bridge (${e?.message||e}) — retrying next tick.\n`)}catch{}}),xe)try{v.send({t:"update-available",version:xe})}catch{}}if("1"!==process.env.TP_PAIRBUS_OFF)for(const e of r){if(!e?.code||!ye.has(e.code))continue;const r=Array.isArray(e.participants)?e.participants:[],t=2===r.length?r.find(e=>e&&e!==ce.user.id):null;if(!t){Se.delete(e.code);continue}Se.set(e.code,t);const o=N(ce.user.id,t);o&&hr(t,o)}Ke()};await gr();const wr=setInterval(gr,15e3);Xe=!0,Ze(),c();const br=[];let kr=!1,_r=0,$r=0,Rr=!1;const Sr=setInterval(async()=>{if(kr)return;const e=Date.now();if(!claimLoopWedged({lastClaimOkAt:sr,now:e}))return _r=0,void($r=0);if(Rr||e<$r)return;Rr=!0;const r=k(_r);$r=e+r,_r+=1,console.log(`claim_watchdog sup=${Ee} quiet_ms=${e-sr} attempt=${_r} next_ms=${r} — reconnecting realtime + re-claiming`);try{ae.realtime?.connect?.()}catch{}yr=0;try{await dr(),Ke()}catch{}finally{Rr=!1}},2e4);Sr.unref?.();const Tr=setInterval(()=>{if(kr)return;const e=Date.now(),r=w({claimHeld:tr,lastClaimOkAt:sr,now:e});let t=!1;try{t=!!Ue.presenceState?.()[Ne]}catch{t=!1}const o=b({tracking:r,subscribedAt:Math.max(Ve.subscribedAt,sr),syncSeenAt:Ve.syncSeenAt,present:t,missingSince:ir,now:e});if(ir=o.missingSince,!ir)return void(ar=0);const n=o.verdict;if("recycle"===n){if(ar===ir)return;ar=ir,console.log(`presence_watchdog sup=${Ee} action=recycle missing_ms=${e-ir} track=${He} — full realtime disconnect+reconnect`);try{ae.realtime?.disconnect?.()}catch{}try{ae.realtime?.connect?.()}catch{}try{Ke()}catch{}}else"respawn"===n&&(console.log(`presence_watchdog sup=${Ee} action=respawn missing_ms=${e-ir} track=${He} — recycle failed, exiting for a clean supervisor respawn`),process.exit(1))},15e3);Tr.unref?.();const Ir=e=>{if(kr)return;kr=!0;const r=De?1:0;console.log(`supervisor_stop sup=${Ee} reason=${e||"unknown"} restarting=${De} children=${ye.size}`),clearInterval(wr),clearInterval(Sr),clearInterval(Tr);for(const e of br)try{clearInterval(e)}catch{}rr.cancel(),W({pid:process.pid}),releaseSingleton();for(const r of ye.values())try{r.kill(e||"SIGTERM")}catch{}lr.close();for(const{channel:e}of Te.values())try{ae.removeChannel(e)}catch{}(async()=>{try{tr&&await ae.rpc("release_bridge",{p_bridge_id:X})}catch{}try{await Ue.untrack()}catch{}try{await ae.removeChannel(Le)}catch{}try{await ae.removeChannel(Ue)}catch{}process.exit(r)})(),setTimeout(()=>process.exit(r),1500)};for(const e of["SIGINT","SIGTERM","SIGHUP"])process.on(e,()=>Ir(e));async function Or(){if(!Ce&&g({pendingUpdate:xe,stopping:kr,childIdle:ve,childBetween:we,childPeer:be,requested:Oe})){if(Ce=!0,!await function(e=2500){const r=[...ye.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(()=>{Ae.delete(o),t(!1)},e),n.timer.unref?.(),Ae.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),Ae.delete(o),t(n.ok))})}())return Ce=!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 ${xe} ready (running ${te}) — restarting account bridge to update; sessions resume.\n`);try{const e=await import("./service.mjs");if(e.serviceActive(null)&&!1!==e.applyStagedServiceUpdate(null,xe))return}catch{}if(Ce=!1,De=!0,!me())try{e(process.execPath,[process.argv[1]],{detached:!0,stdio:"ignore",env:process.env}).unref()}catch{}Ir("SIGTERM")}}if("1"===process.env.THINKPOOL_PAIR_AUTOUPDATE&&te){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,te)){xe||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`)}xe=e;for(const r of ye.values())try{r.send({t:"update-available",version:e})}catch{}Or()}};br.push(setInterval(o,e).unref()),br.push(setInterval(Or,15e3).unref()),br.push(setTimeout(o,6e4).unref())}await new Promise(()=>{})}
@@ -0,0 +1 @@
1
+ import{randomUUID as r}from"node:crypto";import t from"node:fs";import e from"node:os";import o from"node:path";const i=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;export function loadOrCreateBridgeId(n){const c=n||process.env.TP_PAIR_ROOT||o.join(e.homedir(),".thinkpool-pair"),a=o.join(c,"bridge-id");try{const r=t.readFileSync(a,"utf8").trim();if(i.test(r))return r}catch{}const s=r();try{t.mkdirSync(c,{recursive:!0});const r=`${a}.tmp-${process.pid}`;t.writeFileSync(r,s,{flag:"wx"}),t.renameSync(r,a)}catch(r){try{const r=t.readFileSync(a,"utf8").trim();if(i.test(r))return r}catch{}if(!t.existsSync(a))throw r}return s}export function bridgeLabel(r=e.hostname()){return String(r||"bridge").split(".")[0].replace(/[^a-zA-Z0-9-_]+/g,"-")}