grix-connector
Version:
Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.
2 lines (1 loc) • 5.76 kB
JavaScript
import{existsSync as h,mkdirSync as U,readFileSync as b,renameSync as I,writeFileSync as P}from"node:fs";import{join as m,dirname as $}from"node:path";import{spawn as E}from"node:child_process";import{log as n}from"../log/index.js";import{GRIX_PATHS as _}from"../log/index.js";import{resolveClientVersion as u}from"../util/client-version.js";import{UpgradeError as M,collectEnvInfo as D,getUpgradeLogTail as L,npmInstall as O,pendingExists as y,preflightCheck as N,readPending as R,removePending as f,upgradeLog as a,verifyInstalledVersion as F,writePending as x}from"./npm-upgrader.js";const B=360*60*1e3,G=300*1e3,v=1800*1e3,V=2,j=3,H="grix-connector",w=1e4;function S(o){return o.replace(/^wss:/,"https:").replace(/^ws:/,"http:")}function k(){return m(_.data,"upgrade-state.json")}function T(){const o=k();if(!h(o))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(b(o,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function K(o){const t=k();U($(t),{recursive:!0});const e=t+".tmp";P(e,JSON.stringify(o),"utf-8"),I(e,t)}function A(){return new Date().toISOString().slice(0,10)}class Z{agentConfigs;isBusy;timer=null;initialTimer=null;running=!1;stopped=!1;constructor(t,e){this.agentConfigs=t,this.isBusy=e}async start(){await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),B)))},G)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){const t=this.agentConfigs[0];return t?this.queryUpgrade(t):{available:!1}}async handlePendingOnStartup(){if(!y())return;const t=R();if(!t){f();return}const e=u();e===t.target_version?(a(`daemon startup: version matches target ${t.target_version}, upgrade succeeded`),await this.reportUpgrade({from_version:t.from_version,to_version:t.target_version,status:"success"})):(a(`daemon startup: version ${e} != target ${t.target_version}, rolled back`),await this.reportUpgrade({from_version:t.from_version,to_version:t.target_version,status:"rolled_back",error_code:"STARTUP_CRASH",crash_count:t.crash_count})),f()}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(t){n.error("upgrade",`Check failed: ${t instanceof Error?t.message:t}`)}finally{this.running=!1}}}async check(){if(y())return;const t=this.agentConfigs[0];if(!t)return;const e=await this.queryUpgrade(t);if(!e.available||!e.release)return;const r=e.release.version,i=u();if(!e.release.force&&!this.checkRateLimit(r))return;const l=N();if(!l.ok){await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:l.errorCode,error_msg:l.errorMsg});return}a(`upgrade start: ${i} -> ${r}`);const g=Date.now();try{if(x(i,r),await O(H,r),F(r),this.startGuardian(),a("npm install verified, reporting installed"),await this.reportUpgrade({from_version:i,to_version:r,status:"installed",duration_ms:Date.now()-g}),a("shutting down for restart"),this.isBusy?.()){a("active tasks detected, waiting for completion before restart");const c=3600*1e3,d=5e3,p=Date.now()+c;for(;this.isBusy()&&Date.now()<p&&!this.stopped;)await new Promise(C=>setTimeout(C,d));if(this.stopped){a("upgrade aborted: checker stopped during wait");return}this.isBusy()?a("active tasks still running after 1h max wait, forcing restart"):a("all tasks completed, proceeding with restart")}process.kill(process.pid,"SIGTERM")}catch(c){const d=c instanceof M?c.code:"NPM_INSTALL_FAILED",p=c instanceof Error?c.message:String(c);a(`upgrade failed: ${d} ${p}`),f(),this.recordFailure(r),await this.reportUpgrade({from_version:i,to_version:r,status:"failed",error_code:d,error_msg:p,duration_ms:Date.now()-g,upgrade_log:L()})}}checkRateLimit(t){const e=T();if(e.last_failure_at){const l=Date.now()-new Date(e.last_failure_at).getTime();if(l<v)return n.info("upgrade",`In cooldown, ${(v-l)/6e4}m remaining`),!1}const r=e.version_attempts[t]??0;if(r>=j)return n.info("upgrade",`Version ${t} already tried ${r} times, skipping`),!1;const i=A(),s=e.daily_attempts[i]??0;return s>=V?(n.info("upgrade",`Already ${s} attempts today, skipping`),!1):!0}recordFailure(t){const e=T(),r=A();e.last_failure_at=new Date().toISOString(),e.last_failure_version=t,e.daily_attempts[r]=(e.daily_attempts[r]??0)+1,e.version_attempts[t]=(e.version_attempts[t]??0)+1,K(e)}async queryUpgrade(t){const r=`${S(t.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:u(),channel:t.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const i=await fetch(r,{headers:{Authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(w)});if(!i.ok)return n.warn("upgrade",`Check API returned ${i.status}`),{available:!1};const s=await i.json();return s.code!==0?{available:!1}:s.data}catch(i){return n.warn("upgrade",`Check API error: ${i instanceof Error?i.message:i}`),{available:!1}}}async reportUpgrade(t){const e=this.agentConfigs[0];if(!e)return;const i=`${S(e.wsUrl)}/v1/agent-api/upgrade/report`,s=t.npm_version?t:{...t,...D()};try{await fetch(i,{method:"POST",headers:{Authorization:`Bearer ${e.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(s),signal:AbortSignal.timeout(w)})}catch{}}startGuardian(){const t=m(_.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!h(t)){n.info("upgrade","Guardian not available on this platform, skipping");return}try{const e=E(t,[],{detached:!0,stdio:"ignore"});e.unref(),a(`guardian started (pid ${e.pid})`)}catch(e){n.warn("upgrade",`Failed to start guardian: ${e instanceof Error?e.message:e}`)}}}export{Z as UpgradeChecker};