grix-connector 4.9.2 → 4.9.3
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/dist/assets/dsh-bridge/{grix-dsh-bridge-4.9.2.tgz → grix-dsh-bridge-4.9.3.tgz} +0 -0
- package/dist/assets/dsh-bridge/manifest.json +4 -4
- package/dist/bridge/adapter-pool.js +1 -1
- package/dist/bridge/bridge.js +2 -2
- package/dist/bridge/connector-ops.js +1 -1
- package/dist/bridge/lifecycle-barrier.js +1 -1
- package/dist/core/upgrade/idle-wait.js +1 -0
- package/dist/core/upgrade/upgrade-checker.js +2 -2
- package/dist/grix.js +8 -8
- package/dist/launcher.mjs +103 -1
- package/dist/manager.js +1 -1
- package/dist/mcp/stream-http/security.js +1 -1
- package/dist/runtime/daemon-lock.js +2 -2
- package/dist/service/platform-adapter.js +29 -29
- package/dist/service/process-control.js +1 -1
- package/package.json +1 -1
- package/scripts/launcher.mjs +103 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import I from"node:path";import{existsSync as H}from"node:fs";import{AuditLocalActionHandler as U}from"../audit/query/audit-local-actions.js";import{AuditReplayQueryService as D}from"../audit/query/audit-replay-query-service.js";import{resolveDefaultAuditStorageRootDir as C}from"../audit/runtime/default-audit-recorder.js";import{FileAuditReplayStore as T}from"../audit/storage/file-audit-replay-store.js";import{describeIdleWait as v,isIdleWaitDeferred as N}from"../core/upgrade/idle-wait.js";import{getProxyManager as O,relayHostsForClientType as k,buildGatewayRoute as G}from"../core/proxy/index.js";import{fetchRelayCredentialWithRetry as x,deriveGatewayBaseUrls as L,RelayFetchError as q}from"../core/proxy/relay-credential-fetch.js";import{isDirectRelayExplicitlyUnsupported as B}from"../core/proxy/relay-credential.js";import{RelayStateSyncer as F}from"../core/proxy/relay-state-sync.js";import{supportsKiroCwRelay as M}from"../core/config/provider-env.js";import{log as m,GRIX_PATHS as P}from"../core/log/index.js";const te=["recycleAdaptersForRelayChange","waitUntilLifecycleIdle","getAuditLocalActionHandler","getRelayStateSyncer","isSharedInstance"],ne=["name","config","aibotConfig","aibotHandle","auditLocalActionHandler","providerConfigHandler","relayStateApplyPorts","relayStateSyncer","daemonShutdownRequester","lifecycleAdmissionRestorer"];async function ae(e,t){const r=t.params??{},s=String(r.api_key??"").trim(),o=String(r.anthropic_base_url??"").trim(),n=String(r.openai_base_url??"").trim(),l=typeof r.provider_id=="string"&&r.provider_id.trim().toLowerCase()||void 0,a=typeof r.quota_base_url=="string"&&r.quota_base_url.trim()||void 0,u=typeof r.model=="string"&&r.model.trim()||void 0;let g=!1,f;if(!s){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"MISSING_API_KEY",error_msg:"api_key is required"});return}const _=e.config.adapterType??"acp",p=_==="claude"||_==="codex",y=p&&B(r.direct_relay,_),R=p&&r.direct_relay!==void 0&&!y;try{if(R){if(!e.relayStateApplyPorts){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"unsupported",error_code:"UNSUPPORTED_CLIENT_TYPE",error_msg:"relay state applier is not configured"});return}const i=await e.relayStateApplyPorts.applyEnable({apiKey:s,...o?{anthropicBaseUrl:o}:{},...n?{openaiBaseUrl:n}:{},...u?{model:u}:{},directRelay:r.direct_relay},u);i&&(f=i,g=i.busy||i.pending!==void 0)}else if(p){const i=_==="claude"?o:n;if(!i){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"MISSING_TARGET_BASE_URL",error_msg:`${_==="claude"?"anthropic_base_url":"openai_base_url"} is required`});return}const c=O();if(!c){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"PROXY_UNAVAILABLE",error_msg:"MITM proxy manager not initialized"});return}const d=k(_),E=G(_,i,s,{model:u});c.setRoute(E);for(const S of d)c.setHostDefaultRoute(S,E.routeKey);await c.setAgentRelayEnabled(e.name,!0,{relayHosts:d}),g=!await e.recycleAdaptersForRelayChange()}else{if(!e.providerConfigHandler){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"unsupported",error_code:"UNSUPPORTED_CLIENT_TYPE",error_msg:`client type "${_}" does not support Grix relay provider config`});return}const i=e.config.aibot.clientType,c=M(i),d=c?o:n;if(!d){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"MISSING_TARGET_BASE_URL",error_msg:`${c?"anthropic_base_url":"openai_base_url"} is required`});return}if(c&&!u){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"MISSING_MODEL",error_msg:'model is required for client type "kiro"'});return}await e.providerConfigHandler({...l?{provider_id:l}:{},base_url:d||void 0,...a?{quota_base_url:a}:{},api_key:s,model:u})}e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"ok",result:{accepted:!0,restart_deferred:g,...f?{restarted:f.restarted,busy:f.busy,...f.pending?{pending:f.pending}:{}}:{}}})}catch(i){m.error(e.name,`handleConfigureGatewayProvider failed: ${i}`),e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"CONFIGURE_FAILED",error_msg:i instanceof Error?i.message:String(i)})}}function ie(e){if(!e.auditLocalActionHandler){const t=new T({rootDir:C()});e.auditLocalActionHandler=new U(new D(t))}return e.auditLocalActionHandler}async function oe(e,t){const r=await e.getAuditLocalActionHandler().handle(t.action_type,t.params);if(r.status==="ok"){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"ok",result:r.result});return}e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:r.errorCode,error_msg:r.errorMsg})}async function se(e,t){let r=null,s=!1,o=!1,n=String((t.params??{}).target_version??"").trim();const l=String((t.params??{}).reason??"server_initiated");if(!n){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"MISSING_TARGET_VERSION",error_msg:"target_version is required for connector_rollback"});return}m.info(e.name,`connector_rollback: target=${n} reason=${l}`);try{const{UpgradeError:a,armWindowsUpgradeHandoff:u,normalizeUpgradeVersion:g,npmInstall:f,pendingExists:_,upgradeLog:p,verifyInstalledVersion:y,writePending:R}=await import("../core/upgrade/npm-upgrader.js"),{resolveClientVersion:i}=await import("../core/util/client-version.js");n=g(n);const c=i(),d=process.platform==="win32";if(_())throw new a("UPGRADE_IN_PROGRESS","Another connector upgrade transaction is already active");if(d){const b=["upgrade-guardian.mjs","upgrade-guardian.fallback.mjs"].map(A=>I.join(P.base,"bin",A)).find(A=>H(A));if(process.env.GRIX_WRAPPER_SUPERVISED!=="1"||!b||!e.daemonShutdownRequester)throw new a("WINDOWS_UPGRADE_UNSUPPORTED","Windows rollback requires a refreshed managed wrapper and external guardian")}p(`server rollback: ${c} -> ${n} reason=${l}`);const S=R(c,n,d?"preparing":void 0).transaction_id;if(!S)throw new a("INVALID_UPGRADE_STATE","Upgrade transaction has no owner ID");r=S,(await f("grix-connector",n))?.activationRequired||y(n),e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"ok",result:{accepted:!0,target_version:n}}),o=!0,s=!0;const w=await e.waitUntilLifecycleIdle("connector_rollback");if(w.outcome!=="idle"){const b=v("connector-rollback handoff",w);throw p(`server rollback: ${b}`),new a(N(w.outcome)?"UPGRADE_DEFERRED":"UPGRADE_ABORTED",b)}if(d){if(!["upgrade-guardian.mjs","upgrade-guardian.fallback.mjs"].some(A=>H(I.join(P.base,"bin",A))))throw new a("WINDOWS_UPGRADE_UNSUPPORTED","Windows external guardian disappeared before lifecycle handoff");u(process.pid,S),await e.daemonShutdownRequester("upgrade")}else process.kill(process.pid,"SIGTERM")}catch(a){try{const{discardPendingStaging:f,finalizePendingTransaction:_,readPending:p,removePending:y,removeWindowsHandoffFiles:R,rollbackPendingInstall:i}=await import("../core/upgrade/npm-upgrader.js"),c=p(),d=!!(r&&c?.transaction_id===r);d&&process.platform==="win32"?(R(),f(c)):d&&i(c),d&&(process.platform==="win32"?_(c):y(r))}catch{}s&&e.lifecycleAdmissionRestorer?.();const u=a instanceof Error?a.message:String(a);a instanceof Error&&"code"in a&&a.code==="UPGRADE_DEFERRED"?m.info(e.name,`connector_rollback deferred: ${u}`):m.error(e.name,`connector_rollback failed: ${u}`),o||e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:a instanceof Error&&"code"in a&&typeof a.code=="string"?a.code:"ROLLBACK_FAILED",error_msg:u})}}function ce(e){const t=e.relayStateApplyPorts;return!t||!e.aibotHandle||e.isSharedInstance()?null:(e.relayStateSyncer||(e.relayStateSyncer=new F({...t,sendSyncRequest:(r,s)=>{const{anthropicBaseUrl:o,openaiBaseUrl:n}=L(e.aibotConfig.url);return e.aibotHandle.relayStateSyncRequest({anthropic_base_url:o,openai_base_url:n,...r},s)},sendCredentialRequest:(r,s)=>{const{anthropicBaseUrl:o,openaiBaseUrl:n}=L(e.aibotConfig.url);return e.aibotHandle.relayCredentialRequest({anthropic_base_url:o,openai_base_url:n,...r},s)},sendReport:r=>e.aibotHandle.sendRelayStateReport(r),log:{info:r=>m.info(e.name,r),warn:r=>m.warn(e.name,r)}})),e.relayStateSyncer)}function le(e){const t=e.getRelayStateSyncer();t&&t.syncOnConnect().catch(r=>{m.warn(e.name,`relay state sync failed: ${r instanceof Error?r.message:String(r)}`)})}function de(e){try{e.getRelayStateSyncer()?.reportLocalChange()}catch{}}async function ue(e,t){const r=t.params??{},s=r.enabled,o=Number(r.revision);if(typeof s!="boolean"||!Number.isFinite(o)){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"INVALID_PARAMS",error_msg:"enabled (boolean) and revision (number) are required"});return}const n=e.getRelayStateSyncer();if(!n){e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:"failed",error_code:"RELAY_UNAVAILABLE",error_msg:"relay state sync is not available on this agent",result:{revision:o}});return}const l=await n.applyFromLocalAction({enabled:s,model:typeof r.model=="string"?r.model:void 0,revision:o});e.aibotHandle.sendLocalActionResult({action_id:t.action_id,status:l.ok?"ok":"failed",result:{revision:l.revision},...l.error_code?{error_code:l.error_code}:{},...l.error_msg?{error_msg:l.error_msg}:{}})}async function _e(e,t){const r=e.aibotHandle;if(!r||r.status!=="ready")throw new q("agent websocket is not connected","OFFLINE");const{anthropicBaseUrl:s,openaiBaseUrl:o}=L(e.aibotConfig.url);return x((n,l)=>r.relayCredentialRequest(n,l),{anthropic_base_url:s,openai_base_url:o,...t.model?{model:t.model}:{}})}function fe(e){return!!e.config.aibot.sharedOwnerId}export{ne as CONNECTOR_OPS_HOST_FIELDS,te as CONNECTOR_OPS_HOST_METHODS,_e as fetchRelayCredential,ie as getAuditLocalActionHandler,ce as getRelayStateSyncer,ue as handleApplyRelayState,oe as handleAuditLocalAction,ae as handleConfigureGatewayProvider,se as handleConnectorRollback,fe as isSharedInstance,le as relayStateSyncOnConnect,de as reportRelayStateLocalChange};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const n=6e4;class o{closed=!1;
|
|
1
|
+
const n=6e4;class o{closed=!1;admissions=new Map;admissionSeq=0;autoOpenTimer=null;closedAt=0;onAutoOpen=null;close(e=6e4){const s=this.closed&&this.autoOpenTimer!==null;this.closed=!0,!s&&(this.closedAt=Date.now(),!(!Number.isFinite(e)||e<=0)&&(this.autoOpenTimer=setTimeout(()=>{if(this.autoOpenTimer=null,!this.closed)return;const t=Date.now()-this.closedAt;this.closed=!1,this.onAutoOpen?.(t)},e),this.autoOpenTimer.unref?.()))}open(){this.closed=!1,this.clearAutoOpen()}setAutoOpenHandler(e){this.onAutoOpen=e}isClosed(){return this.closed}hasActiveAdmissions(){return this.admissions.size>0}describeActiveAdmissions(){const e=Date.now();return[...this.admissions.values()].map(({label:s,since:t})=>({label:s,heldMs:e-t}))}tryEnter(e="session-control"){if(this.closed)return null;const s=++this.admissionSeq;this.admissions.set(s,{label:e,since:Date.now()});let t=!1;return()=>{t||(t=!0,this.admissions.delete(s))}}clearAutoOpen(){this.autoOpenTimer&&(clearTimeout(this.autoOpenTimer),this.autoOpenTimer=null)}}export{o as LifecycleBarrier};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const b=3,h=5;function g(e){return e.length<=16?e:`${e.slice(0,6)}..${e.slice(-4)}`}function f(e){const n=[e.agent,e.session?g(e.session):null].filter(Boolean).join("/"),t=e.count&&e.count>1?` x${e.count}`:"",i=e.label?` [${e.label}]`:"",s=e.suspect?` (suspect: ${e.suspect})`:"";return`${n||"?"} ${e.source}${t}${i}${s}`}function r(e,n=3){if(e.length===0)return"none";const t=[...e.filter(s=>s.suspect),...e.filter(s=>!s.suspect)],i=t.slice(0,n).map(f).join("; ");return t.length<=n?i:`${i}; +${t.length-n} more (${t.length} total)`}const w=5e3,$=600*1e3;function m(e){return e==="timeout"||e==="busy-again"}function u(e){if(e<1e3)return`${Math.max(0,Math.round(e))}ms`;const n=Math.round(e/1e3),t=Math.floor(n/60),i=n%60;return t>0?`${t}m${i}s`:`${i}s`}function p(e,n){const t=u(n.waitedMs),i=u(n.budgetMs),s=n.busy?.length?`; busy: ${r(n.busy)}`:"";switch(n.outcome){case"timeout":return`deferred at ${e}: machine still busy after waiting ${t} of a ${i} budget; admission was never closed so agents kept serving, next cycle retries`+s;case"busy-again":return`deferred at ${e}: new work arrived while closing admission after waiting ${t} (budget ${i}); admission reopened, next cycle retries`+s;case"stopped":return`aborted at ${e}: connector stopped while waiting for idle (waited ${t} of a ${i} budget)`;case"idle":return`idle at ${e} after ${t} (budget ${i})`}}async function y(e){const n=Date.now(),t=e.budgetMs,i=u(t),s=(l,a)=>({outcome:l,waitedMs:Date.now()-n,budgetMs:t,...a&&a.length>0?{busy:a}:{}});let o=e.describeBusy();if(o.length>0){e.log("info",`active tasks detected, waiting up to ${i} until idle before restart (admission stays open while waiting); busy: ${r(o,5)}`);const l=n+t;let a=Date.now();for(;o.length>0&&!e.isStopped();){if(Date.now()>=l)return e.log("warn",`idle wait exceeded ${i}; abandoning this attempt. Admission was never closed, so agents kept serving. A busy state that never clears usually means an adapter leaked its busy flag \u2014 check for a turn that never completed. Still busy: ${r(o,5)}`),s("timeout",o);await new Promise(d=>setTimeout(d,w)),o=e.describeBusy(),o.length>0&&Date.now()-a>=$&&(a=Date.now(),e.log("info",`still waiting for active tasks to finish before restart; busy: ${r(o,5)}`))}}if(e.isStopped())return s("stopped");e.closeAdmission();const c=e.describeBusy();return c.length>0?(e.log("warn",`work arrived while closing admission; reopening and abandoning this attempt; busy: ${r(c,5)}`),e.reopenAdmission(),s("busy-again",c)):(e.log("info","all tasks completed, proceeding with restart"),s("idle"))}export{p as describeIdleWait,r as formatBusyReasons,u as formatWaitDuration,m as isIdleWaitDeferred,y as waitForIdle};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{closeSync as x,existsSync as m,mkdirSync as F,openSync as me,readFileSync as P,realpathSync as we,renameSync as ve,writeFileSync as ye}from"node:fs";import{join as w,dirname as be}from"node:path";import{spawn as q}from"node:child_process";import{log as v}from"../log/index.js";import{GRIX_PATHS as y}from"../log/index.js";import{resolveClientVersion as A}from"../util/client-version.js";import{UpgradeError as k,armWindowsUpgradeHandoff as B,archivePendingJournal as Se,armWindowsUpgradeRollback as Te,cleanupPendingBackup as z,collectEnvInfo as ke,discardPendingStaging as N,finalizePendingTransaction as j,getUpgradeLogTail as E,isVersionAvailable as K,npmInstall as Ae,normalizeUpgradeVersion as $e,pendingExists as X,preflightCheck as Y,readPending as M,readPendingPackageVersion as J,removeWindowsHandoffFiles as V,rollbackPendingInstall as Re,updatePending as G,upgradeLog as a,verifyInstalledVersion as Pe,writePending as Ee}from"./npm-upgrader.js";import{isRetriableRegistryError as Z,isTransientInstallError as Q}from"../installer/npm-registry.js";import{installVersionIntoStore as Ie}from"./npm-upgrader.js";import{ensureLauncher as Ue,healthMarkerPath as De,markHealthy as Ne,pruneVersions as Me,readPointer as $,readRollbackRecord as Ge,readSwitchRecord as Le,resolveLauncherStore as ee,rollbackRecordPath as L,switchCurrent as Oe,switchRecordPath as O,takeCrashNote as te,versionDir as re,versionEntry as I,writeSwitchRecord as ae}from"./version-store.js";import{rmSync as T}from"node:fs";import{randomUUID as Ce}from"node:crypto";const We=360*60*1e3,He=300*1e3,ie=1800*1e3,xe=2,Fe=3,qe=10,U="grix-connector",ne=1e4,C=15,se=2e3,oe=8e3,Be=1e3,ze=2e3,je=120*1e3,ce=1e4,Ke=6e4,Xe=3,Ye=600*1e3,Je=9e4,Ve=3e4,Ze=9e4,Qe=3600*1e3,et=new Set(["preparing","staged","handoff_ready","activating","verifying","healthy","rolling_back","rolled_back","finalizing","failed"]),W=600*1e3,H=1800*1e3;function D(c,e=W){const t=Date.parse(c.activation_started_at??c.upgraded_at??"");return Number.isNaN(t)?!0:Date.now()-t>e}function de(c){const e=Date.parse(c.verification_started_at??c.activation_started_at??"");return Number.isNaN(e)||Date.now()-e>je}function tt(c){if((c.guardian_handback_count??0)>=Xe)return!1;const e=Date.parse(c.guardian_handback_at??"");return Number.isNaN(e)?!0:Date.now()-e>=Ye}function rt(c){if(!Number.isInteger(c)||(c??0)<=0)return!1;try{return process.kill(c,0),!0}catch{return!1}}function le(c){try{return we(c)}catch{return c}}function ue(){try{F(w(y.base,"log"),{recursive:!0});const c=me(w(y.base,"log","upgrade.log"),"a");return{stdio:["ignore",c,c],logFd:c}}catch{return{stdio:"ignore"}}}function R(){const c=w(y.base,"bin"),e=w(c,"upgrade-guardian.mjs");if(m(e))return le(e);const t=w(c,"upgrade-guardian.fallback.mjs");return m(t)?le(t):null}function he(c){if(!rt(c.guardian_pid))return!1;try{const t=JSON.parse(P(w(y.data,"upgrade-monitor-heartbeat.json"),"utf-8"));if(t.transaction_id===c.transaction_id&&t.pid===c.guardian_pid&&typeof t.updated_at=="number"&&Date.now()-t.updated_at<=ce)return!0}catch{}const e=Date.parse(c.guardian_started_at??"");return Number.isFinite(e)&&Date.now()-e<=ce}function pe(c){return new URL(c.replace(/^wss:/,"https:").replace(/^ws:/,"http:")).origin}function fe(){return w(y.data,"upgrade-state.json")}function ge(){const c=fe();if(!m(c))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(P(c,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function at(c){const e=fe();F(be(e),{recursive:!0});const t=e+".tmp";ye(t,JSON.stringify(c),"utf-8"),ve(t,e)}function _e(){return new Date().toISOString().slice(0,10)}class wt{agentConfigs;isBusy;closeAdmission;options;timer=null;initialTimer=null;pendingTimer=null;healthyTimer=null;guardianWaitLoggedAt=0;guardianWaitLoggedPhase="";recordedRollbackFailures=new Set;posixGuardianResumeAttempted=new Set;running=!1;stopped=!1;constructor(e,t,r,i={}){this.agentConfigs=e,this.isBusy=t,this.closeAdmission=r,this.options=i}async start(){await this.handleLauncherStartup(),await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),We)))},He)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null),this.pendingTimer&&(clearTimeout(this.pendingTimer),this.pendingTimer=null),this.healthyTimer&&(clearTimeout(this.healthyTimer),this.healthyTimer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){return this.agentConfigs.length===0?{available:!1}:(await Promise.all(this.agentConfigs.map(r=>this.queryUpgrade(r)))).find(r=>r.available&&r.release)??{available:!1}}async handlePendingOnStartup(){if(!X())return;let e=M();if(!e){a("daemon startup: pending journal is unreadable; preserving it and blocking automatic upgrade for manual recovery");return}if(e.phase&&!et.has(e.phase)&&(e=G({phase:"failed",error_code:"INVALID_UPGRADE_STATE",error_msg:`Unknown upgrade transaction phase: ${String(e.phase)}`},e.transaction_id)),e.phase==="finalizing"){this.settleTransaction(e)||(a(`terminal transaction ${e.transaction_id??"legacy"} is waiting for its shared lock to be released`),this.schedulePendingReconcile());return}if(e.phase==="preparing"||e.phase==="staged"){const d=!!(e.wrapper_pause_file&&m(e.wrapper_pause_file));if(e.phase==="staged"&&d){this.schedulePendingReconcile();return}N(e),e=G({phase:"rolled_back",error_code:"UPGRADE_INTERRUPTED",error_msg:"Upgrade stopped before Windows activation started"})}if(e.phase==="verifying"){if(he(e)&&!de(e)){this.schedulePendingReconcile();return}if(de(e)&&e.transaction_id&&["win32","darwin"].includes(this.options.platform??process.platform)&&this.options.requestGracefulShutdown){const d=`Target ${e.target_version} did not become healthy before the Windows verification deadline`;Te(e.transaction_id,d),a(`${d}; requesting graceful shutdown for offline rollback`),await this.options.requestGracefulShutdown("upgrade");return}await this.resumeWindowsGuardian(),this.schedulePendingReconcile();return}const t=new Set(["handoff_ready","activating","rolling_back"]);if(e.phase&&t.has(e.phase)){if(he(e)){this.logGuardianWait(e),this.schedulePendingReconcile();return}if(D(e)&&e.transaction_id&&(this.options.platform??process.platform)==="win32"&&this.options.requestGracefulShutdown&&tt(e)){const d=`Guardian did not settle ${e.phase} for ${e.target_version} before the Windows activation deadline`;G({guardian_handback_count:(e.guardian_handback_count??0)+1,guardian_handback_at:new Date().toISOString()},e.transaction_id),a(`${d}; requesting graceful shutdown so the wrapper re-runs the guardian`),await this.options.requestGracefulShutdown("upgrade");return}if(D(e))a(`daemon startup: guardian did not settle ${e.phase} for ${e.target_version} and cannot be resumed; collapsing the transaction by version verdict`),V(),N(e);else{await this.resumeWindowsGuardian(),this.logGuardianWait(e),this.schedulePendingReconcile();return}}if(e.phase==="failed"){const d=J(e),_=A();if(!d||d!==_){a(`daemon startup: upgrade transaction requires manual recovery: ${e.error_msg??e.error_code??"unknown error"} (runtime=${_}, active package=${d??"unknown"})`);return}const l=Se(e);a(`daemon startup: failed transaction ${e.transaction_id??"legacy"} (${e.error_code??"unknown error"}) is converging on the runnable ${_}${l?`; diagnostics kept at ${l}`:""}`)}const r=J(e),i=A(),s=e.phase==="healthy",u=e.phase==="rolled_back",o=!r||r===e.target_version,n=s?i===e.target_version&&r===e.target_version:!u&&i===e.target_version&&o;if(!n&&o&&!u){if(!D(e)){if((this.options.platform??process.platform)!=="win32"&&r===e.target_version){const d=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.posixGuardianResumeAttempted.has(d)||(this.posixGuardianResumeAttempted.add(d),a(`resuming POSIX guardian for stalled activation of ${e.target_version}`),this.startGuardian())}this.logThrottled("posix-activation-wait",`daemon startup: runtime ${i} is waiting for guardian activation of ${e.target_version} (active package=${r??"unknown"})`),this.schedulePendingReconcile();return}a(`daemon startup: activation wait for ${e.target_version} expired (> ${W/6e4}min); no guardian will finish the switch, reporting by version verdict`)}const p=n?{from_version:e.from_version,to_version:e.target_version,status:"success"}:{from_version:e.from_version,to_version:e.target_version,status:"rolled_back",error_code:e.error_code??"STARTUP_CRASH",error_msg:e.error_msg,crash_count:e.crash_count};if(a(n?`daemon startup: version matches target ${e.target_version}, upgrade succeeded`:`daemon startup: runtime=${i}, active package=${r??"unknown"}, target=${e.target_version}; rolled back`),!n){const d=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.recordedRollbackFailures.has(d)||(this.recordedRollbackFailures.add(d),this.recordFailure(e.target_version))}if(n){const d=await this.confirmHealthz(oe,s?e.target_version:void 0,s?e.old_pid:void 0,s?e.activation_started_at:void 0);if(s&&!d){if(!D(e)){a("guardian marked healthy but target-version healthz is not yet observable; retrying reconciliation"),this.schedulePendingReconcile();return}a(`guardian marked healthy for ${e.target_version} but healthz stayed unobservable past the ${W/6e4}min activation window; collapsing the transaction by version verdict`)}else a(d?"healthz confirmed healthy, reporting success":"healthz not confirmed within window; reporting success on version-match (guardian still guards crashes)")}if(!z(e)){a(`terminal transaction ${e.transaction_id??"legacy"} is waiting for antivirus-held artifact cleanup`),this.schedulePendingReconcile();return}if(await this.reportUpgrade(p)){this.settleTransaction(e)||(a(`terminal transaction ${e.transaction_id??"legacy"} could not release its shared lock; retrying reconciliation`),this.schedulePendingReconcile());return}this.retryStartupReport(p,e,!1)}settleTransaction(e){if(!j(e))return!1;try{this.options.onTransactionSettled?.()}catch(t){a(`onTransactionSettled hook failed: ${t instanceof Error?t.message:String(t)}`)}return!0}schedulePendingReconcile(){this.stopped||this.pendingTimer||(this.pendingTimer=setTimeout(()=>{this.pendingTimer=null,!this.stopped&&this.handlePendingOnStartup().catch(e=>{v.warn("upgrade",`Pending reconciliation failed: ${e instanceof Error?e.message:e}`),this.schedulePendingReconcile()})},ze))}logThrottled(e,t){const r=Date.now();e===this.guardianWaitLoggedPhase&&r-this.guardianWaitLoggedAt<Ke||(this.guardianWaitLoggedPhase=e,this.guardianWaitLoggedAt=r,a(t))}logGuardianWait(e){const t=String(e.phase??"");this.logThrottled(`guardian:${t}`,`transaction ${e.transaction_id??"legacy"} is ${t}; waiting for guardian verdict`)}async resumeWindowsGuardian(){if(this.options.resumeWindowsGuardian){await this.options.resumeWindowsGuardian();return}const e=R();if(!e)return;const{stdio:t,logFd:r}=ue();try{q(process.execPath,[e,"activate","--root",y.base],{detached:!0,stdio:t,windowsHide:!0}).unref()}finally{if(r!==void 0)try{x(r)}catch{}}}async retryStartupReport(e,t,r){const i=t?.transaction_id,s=()=>{const u=M();if(!(i&&u?.transaction_id!==i)){if(r&&!z(t)){a(`terminal backup cleanup is still blocked for ${i??"legacy transaction"}`),this.schedulePendingReconcile();return}this.settleTransaction(t)||(a(`terminal shared lock release is still blocked for ${i??"legacy transaction"}`),this.schedulePendingReconcile())}};for(let u=2;u<=C;u++){if(this.stopped)return;const o=se*(u-1)+Math.floor(Math.random()*1e3);if(await new Promise(n=>setTimeout(n,o)),this.stopped)return;if(await this.reportUpgrade(e)){s();return}}a(`startup report (${e.status}) not delivered after ${C} attempts; clearing pending to unblock future checks`),s()}async confirmHealthz(e,t,r,i){let s="19579";try{const n=w(y.data,"health-port");m(n)&&(s=P(n,"utf-8").trim()||s)}catch{}const u=`http://127.0.0.1:${s}/healthz`,o=Date.now()+e;for(;Date.now()<o&&!this.stopped;){try{const n=await fetch(u,{signal:AbortSignal.timeout(2e3)});if(n.ok){if(!t)return!0;const p=await n.json();if(p.status==="ok"&&p.version===t&&typeof p.pid=="number"&&p.pid>0&&p.pid!==r)try{const d=JSON.parse(P(w(y.base,"daemon-status.json"),"utf-8")),_=Date.parse(i??"");return!!(d.state==="running"&&d.pid===p.pid&&typeof d.updated_at=="number"&&Number.isFinite(_)&&d.updated_at>=_)}catch{return!1}}}catch{}await new Promise(n=>setTimeout(n,Be))}return!1}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(e){v.error("upgrade",`Check failed: ${e instanceof Error?e.message:e}`)}finally{this.running=!1}}}async check(){if(X()){await this.handlePendingOnStartup();return}if(this.agentConfigs.length===0)return;const r=(await Promise.all(this.agentConfigs.map(h=>this.queryUpgrade(h)))).find(h=>h.available&&h.release)?.release;if(!r)return;const i=A();let s;try{s=$e(r.version)}catch(h){const g=h;await this.reportUpgrade({from_version:i,to_version:String(r.version),status:"failed",error_code:g.code??"INVALID_VERSION",error_msg:g.message});return}const u=r.force,o=this.options.platform??process.platform,n=this.options.env??process.env,p=ee(n);if(p&&this.options.requestGracefulShutdown){await this.runLauncherUpgrade(p,i,s,!!u);return}if(o==="win32"){const h=R(),g=n.GRIX_WRAPPER_SUPERVISED==="1";if(!g||!h||!this.options.requestGracefulShutdown){const S=`Windows automatic upgrade requires a refreshed service installation (${[...g?[]:["managed wrapper"],...h?[]:["external guardian"],...this.options.requestGracefulShutdown?[]:["graceful shutdown hook"]].join(", ")})`;a(S),await this.reportUpgrade({from_version:i,to_version:s,status:"failed",error_code:"WINDOWS_UPGRADE_UNSUPPORTED",error_msg:S});return}}const d=o==="darwin"&&!!R()&&!!this.options.requestGracefulShutdown;if(o==="darwin"&&!d&&a("macOS transactional upgrade prerequisites missing; falling back to the legacy guardian flow"),!u&&!this.checkRateLimit(s))return;const _=Y();if(!_.ok){await this.reportUpgrade({from_version:i,to_version:s,status:"failed",error_code:_.errorCode,error_msg:_.errorMsg});return}if(!await K(U,s)){a(`target ${s} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}a(`upgrade start: ${i} -> ${s}`);const l=Date.now();let f;try{f=Ee(i,s,o==="win32"||d?"preparing":void 0).transaction_id,(await Ae(U,s,void 0,d?{activationMode:"staged"}:{}))?.activationRequired||Pe(s),a("npm install verified, reporting installed"),await this.reportUpgrade({from_version:i,to_version:s,status:"installed",duration_ms:Date.now()-l}),a("shutting down for restart");const g=await this.waitUntilIdle();if(g!=="idle")throw a(`upgrade aborted before lifecycle handoff: ${g}`),this.options.reopenLifecycleAdmission?.(),new k("UPGRADE_ABORTED",`Upgrade was aborted before lifecycle handoff (${g})`);if(o==="win32"){if(!R())throw new k("WINDOWS_UPGRADE_UNSUPPORTED","Windows external guardian disappeared before lifecycle handoff");B(process.pid,f),a("Windows handoff armed; requesting graceful daemon shutdown"),await this.options.requestGracefulShutdown("upgrade")}else if(d){if(!R())throw new k("MACOS_UPGRADE_UNSUPPORTED","macOS guardian helper disappeared before lifecycle handoff");B(process.pid,f),a("macOS handoff armed; spawning guardian before graceful daemon shutdown"),await this.resumeWindowsGuardian(),await this.options.requestGracefulShutdown("upgrade")}else this.startGuardian(),process.kill(process.pid,"SIGTERM")}catch(h){const g=h instanceof k?h.code:"NPM_INSTALL_FAILED",b=h instanceof Error?h.message:String(h);a(`upgrade failed: ${g} ${b}`);const S=M();if(f&&S?.transaction_id===f&&(o==="win32"||d?(V(),N(S)):Re(S)&&a("upgrade failed before restart; previous package restored atomically"),j(S)||(a(`failed transaction ${f} is waiting for shared lock release`),this.schedulePendingReconcile()),this.options.reopenLifecycleAdmission?.()),g==="UPGRADE_IN_PROGRESS")return;Z(b)||Q(b)||g==="NPM_TIMEOUT"?a(`failure is retriable (${g}); skipping cooldown, will retry next cycle`):this.recordFailure(s),await this.reportUpgrade({from_version:i,to_version:s,status:"failed",error_code:g,error_msg:b,duration_ms:Date.now()-l,upgrade_log:E()})}}async runLauncherUpgrade(e,t,r,i){if(!i&&!this.checkRateLimit(r))return;const s=Y();if(!s.ok){await this.reportUpgrade({from_version:t,to_version:r,status:"failed",error_code:s.errorCode,error_msg:s.errorMsg});return}if(!await K(U,r)){a(`target ${r} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}a(`launcher upgrade start: ${t} -> ${r} (store=${e})`);const u=Date.now();try{m(I(e,r))?a(`${r} already present in version store; skipping install`):await Ie(e,U,r),await this.reportUpgrade({from_version:t,to_version:r,status:"installed",duration_ms:Date.now()-u});const o=await this.waitUntilIdle();if(o!=="idle")throw this.options.reopenLifecycleAdmission?.(),new k("UPGRADE_ABORTED",`Upgrade was aborted before lifecycle handoff (${o})`);const n={transaction_id:Ce(),from_version:t,to_version:r,started_at:new Date().toISOString()};ae(e,{...n,switched:!1}),Oe(e,r),ae(e,{...n,switched:!0}),a(`current pointer switched to ${r}; requesting restart via launcher`),await this.options.requestGracefulShutdown("upgrade-switch")}catch(o){const n=o instanceof k?o.code:"NPM_INSTALL_FAILED",p=o instanceof Error?o.message:String(o);if(a(`launcher upgrade failed: ${n} ${p}`),n==="UPGRADE_ABORTED"){a("machine stayed busy; keeping the verified version dir and retrying next cycle without penalty"),await this.reportUpgrade({from_version:t,to_version:r,status:"failed",error_code:n,error_msg:p,duration_ms:Date.now()-u,upgrade_log:E()});return}if($(e,"current")!==r)try{T(re(e,r),{recursive:!0,force:!0})}catch{}Z(p)||Q(p)||n==="NPM_TIMEOUT"?a(`failure is retriable (${n}); skipping cooldown, will retry next cycle`):this.recordFailure(r),await this.reportUpgrade({from_version:t,to_version:r,status:"failed",error_code:n,error_msg:p,duration_ms:Date.now()-u,upgrade_log:E()})}}async handleLauncherStartup(){const e=this.options.env??process.env,t=ee(e);if(!t)return;const r=A();try{Ue(t,re(t,r))}catch(l){a(`failed to refresh launcher: ${l instanceof Error?l.message:String(l)}`)}const i=Date.now(),s=()=>{if(m(De(t,r)))return!1;const l=$(t,"previous");return!!l&&l!==r&&m(I(t,l))},u=l=>{this.healthyTimer=setTimeout(()=>{if(this.healthyTimer=null,!this.stopped){if(this.options.isConnected&&!this.options.isConnected()){if(Date.now()-i>=Ze&&this.options.requestGracefulShutdown&&s()){a(`${r} has been up ${Math.round((Date.now()-i)/1e3)}s without any agent websocket ready; exiting so the launcher counts a crash and can roll back`),this.options.requestGracefulShutdown("launcher-unhealthy");return}a(`uptime reached but no agent websocket is ready yet; deferring health marker for ${r}`),u(Ve);return}try{Ne(t,r),te(t,r)}catch(f){a(`failed to write health marker: ${f instanceof Error?f.message:String(f)}`)}}},l),this.healthyTimer.unref?.()};u(Je);const o=Ge(t),n=Le(t);if(!o&&!n)return;const p=()=>{try{T(L(t),{force:!0})}catch{}try{T(O(t),{force:!0})}catch{}const l=Me(t,e);l.length>0&&a(`pruned version directories: ${l.join(", ")}`)},d=()=>{const l=g=>{const b=g?Date.parse(g):Number.NaN;return Number.isFinite(b)&&Date.now()-b<Qe},f=!!(o&&o.from_version!==r&&m(I(t,o.from_version))&&l(o.rolled_back_at)),h=!!(n&&n.to_version!==r&&n.from_version!==r&&m(I(t,n.from_version))&&l(n.started_at));if(!f)try{T(L(t),{force:!0})}catch{}if(!h)try{T(O(t),{force:!0})}catch{}(f||h)&&a(`kept launcher records addressed to another installed version (rollback=${f} switch=${h})`)};let _;if(o&&o.from_version===r){const l=`exit_code=${o.exit_code??"n/a"} crashes=${o.crash_count??"n/a"} uptime_ms=${o.uptime_ms??"n/a"}`;a(`daemon startup: launcher rolled back ${o.to_version} -> ${r} (${l})`),this.recordFailure(o.to_version);const f=te(t,o.to_version),h=f?.split(/\r?\n/)[0]?.slice(0,200);_={from_version:o.from_version,to_version:o.to_version,status:"rolled_back",error_code:"STARTUP_CRASH",error_msg:`Target ${o.to_version} crashed before becoming healthy (${l}); launcher restored ${r}`+(h?`; cause: ${h}`:""),crash_count:o.crash_count,upgrade_log:E()+(f?`
|
|
1
|
+
import{closeSync as H,existsSync as w,mkdirSync as F,openSync as we,readFileSync as P,realpathSync as ve,renameSync as ye,writeFileSync as be}from"node:fs";import{join as v,dirname as Se}from"node:path";import{spawn as x}from"node:child_process";import{log as y}from"../log/index.js";import{GRIX_PATHS as b}from"../log/index.js";import{resolveClientVersion as T}from"../util/client-version.js";import{UpgradeError as E,armWindowsUpgradeHandoff as q,archivePendingJournal as Re,armWindowsUpgradeRollback as Ee,cleanupPendingBackup as B,collectEnvInfo as Te,discardPendingStaging as N,finalizePendingTransaction as z,getUpgradeLogTail as A,isVersionAvailable as j,npmInstall as Ae,normalizeUpgradeVersion as $e,pendingExists as K,preflightCheck as X,readPending as G,readPendingPackageVersion as Y,removeWindowsHandoffFiles as J,rollbackPendingInstall as ke,updatePending as M,upgradeLog as a,verifyInstalledVersion as Pe,writePending as De}from"./npm-upgrader.js";import{isRetriableRegistryError as V,isTransientInstallError as Z}from"../installer/npm-registry.js";import{describeIdleWait as Q,isIdleWaitDeferred as ee,waitForIdle as Ue}from"./idle-wait.js";import{installVersionIntoStore as Ie}from"./npm-upgrader.js";import{ensureLauncher as Ne,healthMarkerPath as Ge,markHealthy as Me,pruneVersions as Le,readPointer as $,readRollbackRecord as Oe,readSwitchRecord as Ce,resolveLauncherStore as te,rollbackRecordPath as L,switchCurrent as We,switchRecordPath as O,takeCrashNote as re,versionDir as ie,versionEntry as D,writeSwitchRecord as ae}from"./version-store.js";import{rmSync as R}from"node:fs";import{randomUUID as He}from"node:crypto";const Fe=360*60*1e3,xe=300*1e3,ne=1800*1e3,qe=2,Be=3,ze=10,U="grix-connector",se=1e4,C=15,oe=2e3,ce=8e3,je=1e3,Ke=2e3,Xe=120*1e3,de=1e4,Ye=6e4,Je=3,Ve=600*1e3,Ze=9e4,Qe=3e4,et=9e4,tt=3600*1e3,rt=new Set(["preparing","staged","handoff_ready","activating","verifying","healthy","rolling_back","rolled_back","finalizing","failed"]),W=600*1e3,it=1800*1e3;function I(c,e=W){const t=Date.parse(c.activation_started_at??c.upgraded_at??"");return Number.isNaN(t)?!0:Date.now()-t>e}function le(c){const e=Date.parse(c.verification_started_at??c.activation_started_at??"");return Number.isNaN(e)||Date.now()-e>Xe}function at(c){if((c.guardian_handback_count??0)>=Je)return!1;const e=Date.parse(c.guardian_handback_at??"");return Number.isNaN(e)?!0:Date.now()-e>=Ve}function nt(c){if(!Number.isInteger(c)||(c??0)<=0)return!1;try{return process.kill(c,0),!0}catch{return!1}}function ue(c){try{return ve(c)}catch{return c}}function he(){try{F(v(b.base,"log"),{recursive:!0});const c=we(v(b.base,"log","upgrade.log"),"a");return{stdio:["ignore",c,c],logFd:c}}catch{return{stdio:"ignore"}}}function k(){const c=v(b.base,"bin"),e=v(c,"upgrade-guardian.mjs");if(w(e))return ue(e);const t=v(c,"upgrade-guardian.fallback.mjs");return w(t)?ue(t):null}function pe(c){if(!nt(c.guardian_pid))return!1;try{const t=JSON.parse(P(v(b.data,"upgrade-monitor-heartbeat.json"),"utf-8"));if(t.transaction_id===c.transaction_id&&t.pid===c.guardian_pid&&typeof t.updated_at=="number"&&Date.now()-t.updated_at<=de)return!0}catch{}const e=Date.parse(c.guardian_started_at??"");return Number.isFinite(e)&&Date.now()-e<=de}function fe(c){return new URL(c.replace(/^wss:/,"https:").replace(/^ws:/,"http:")).origin}function ge(){return v(b.data,"upgrade-state.json")}function _e(){const c=ge();if(!w(c))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(P(c,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function st(c){const e=ge();F(Se(e),{recursive:!0});const t=e+".tmp";be(t,JSON.stringify(c),"utf-8"),ye(t,e)}function me(){return new Date().toISOString().slice(0,10)}class St{agentConfigs;describeBusy;closeAdmission;options;timer=null;initialTimer=null;pendingTimer=null;healthyTimer=null;guardianWaitLoggedAt=0;guardianWaitLoggedPhase="";recordedRollbackFailures=new Set;posixGuardianResumeAttempted=new Set;running=!1;stopped=!1;constructor(e,t,r,n={}){this.agentConfigs=e,this.describeBusy=t,this.closeAdmission=r,this.options=n}async start(){await this.handleLauncherStartup(),await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),Fe)))},xe)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null),this.pendingTimer&&(clearTimeout(this.pendingTimer),this.pendingTimer=null),this.healthyTimer&&(clearTimeout(this.healthyTimer),this.healthyTimer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){return this.agentConfigs.length===0?{available:!1}:(await Promise.all(this.agentConfigs.map(r=>this.queryUpgrade(r)))).find(r=>r.available&&r.release)??{available:!1}}async handlePendingOnStartup(){if(!K())return;let e=G();if(!e){a("daemon startup: pending journal is unreadable; preserving it and blocking automatic upgrade for manual recovery");return}if(e.phase&&!rt.has(e.phase)&&(e=M({phase:"failed",error_code:"INVALID_UPGRADE_STATE",error_msg:`Unknown upgrade transaction phase: ${String(e.phase)}`},e.transaction_id)),e.phase==="finalizing"){this.settleTransaction(e)||(a(`terminal transaction ${e.transaction_id??"legacy"} is waiting for its shared lock to be released`),this.schedulePendingReconcile());return}if(e.phase==="preparing"||e.phase==="staged"){const d=!!(e.wrapper_pause_file&&w(e.wrapper_pause_file));if(e.phase==="staged"&&d){this.schedulePendingReconcile();return}N(e),e=M({phase:"rolled_back",error_code:"UPGRADE_INTERRUPTED",error_msg:"Upgrade stopped before Windows activation started"})}if(e.phase==="verifying"){if(pe(e)&&!le(e)){this.schedulePendingReconcile();return}if(le(e)&&e.transaction_id&&["win32","darwin"].includes(this.options.platform??process.platform)&&this.options.requestGracefulShutdown){const d=`Target ${e.target_version} did not become healthy before the Windows verification deadline`;Ee(e.transaction_id,d),a(`${d}; requesting graceful shutdown for offline rollback`),await this.options.requestGracefulShutdown("upgrade");return}await this.resumeWindowsGuardian(),this.schedulePendingReconcile();return}const t=new Set(["handoff_ready","activating","rolling_back"]);if(e.phase&&t.has(e.phase)){if(pe(e)){this.logGuardianWait(e),this.schedulePendingReconcile();return}if(I(e)&&e.transaction_id&&(this.options.platform??process.platform)==="win32"&&this.options.requestGracefulShutdown&&at(e)){const d=`Guardian did not settle ${e.phase} for ${e.target_version} before the Windows activation deadline`;M({guardian_handback_count:(e.guardian_handback_count??0)+1,guardian_handback_at:new Date().toISOString()},e.transaction_id),a(`${d}; requesting graceful shutdown so the wrapper re-runs the guardian`),await this.options.requestGracefulShutdown("upgrade");return}if(I(e))a(`daemon startup: guardian did not settle ${e.phase} for ${e.target_version} and cannot be resumed; collapsing the transaction by version verdict`),J(),N(e);else{await this.resumeWindowsGuardian(),this.logGuardianWait(e),this.schedulePendingReconcile();return}}if(e.phase==="failed"){const d=Y(e),f=T();if(!d||d!==f){a(`daemon startup: upgrade transaction requires manual recovery: ${e.error_msg??e.error_code??"unknown error"} (runtime=${f}, active package=${d??"unknown"})`);return}const l=Re(e);a(`daemon startup: failed transaction ${e.transaction_id??"legacy"} (${e.error_code??"unknown error"}) is converging on the runnable ${f}${l?`; diagnostics kept at ${l}`:""}`)}const r=Y(e),n=T(),s=e.phase==="healthy",u=e.phase==="rolled_back",o=!r||r===e.target_version,i=s?n===e.target_version&&r===e.target_version:!u&&n===e.target_version&&o;if(!i&&o&&!u){if(!I(e)){if((this.options.platform??process.platform)!=="win32"&&r===e.target_version){const d=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.posixGuardianResumeAttempted.has(d)||(this.posixGuardianResumeAttempted.add(d),a(`resuming POSIX guardian for stalled activation of ${e.target_version}`),this.startGuardian())}this.logThrottled("posix-activation-wait",`daemon startup: runtime ${n} is waiting for guardian activation of ${e.target_version} (active package=${r??"unknown"})`),this.schedulePendingReconcile();return}a(`daemon startup: activation wait for ${e.target_version} expired (> ${W/6e4}min); no guardian will finish the switch, reporting by version verdict`)}const h=i?{from_version:e.from_version,to_version:e.target_version,status:"success"}:{from_version:e.from_version,to_version:e.target_version,status:"rolled_back",error_code:e.error_code??"STARTUP_CRASH",error_msg:e.error_msg,crash_count:e.crash_count};if(a(i?`daemon startup: version matches target ${e.target_version}, upgrade succeeded`:`daemon startup: runtime=${n}, active package=${r??"unknown"}, target=${e.target_version}; rolled back`),!i){const d=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.recordedRollbackFailures.has(d)||(this.recordedRollbackFailures.add(d),this.recordFailure(e.target_version))}if(i){const d=await this.confirmHealthz(ce,s?e.target_version:void 0,s?e.old_pid:void 0,s?e.activation_started_at:void 0);if(s&&!d){if(!I(e)){a("guardian marked healthy but target-version healthz is not yet observable; retrying reconciliation"),this.schedulePendingReconcile();return}a(`guardian marked healthy for ${e.target_version} but healthz stayed unobservable past the ${W/6e4}min activation window; collapsing the transaction by version verdict`)}else a(d?"healthz confirmed healthy, reporting success":"healthz not confirmed within window; reporting success on version-match (guardian still guards crashes)")}if(!B(e)){a(`terminal transaction ${e.transaction_id??"legacy"} is waiting for antivirus-held artifact cleanup`),this.schedulePendingReconcile();return}if(await this.reportUpgrade(h)){this.settleTransaction(e)||(a(`terminal transaction ${e.transaction_id??"legacy"} could not release its shared lock; retrying reconciliation`),this.schedulePendingReconcile());return}this.retryStartupReport(h,e,!1)}settleTransaction(e){if(!z(e))return!1;try{this.options.onTransactionSettled?.()}catch(t){a(`onTransactionSettled hook failed: ${t instanceof Error?t.message:String(t)}`)}return!0}schedulePendingReconcile(){this.stopped||this.pendingTimer||(this.pendingTimer=setTimeout(()=>{this.pendingTimer=null,!this.stopped&&this.handlePendingOnStartup().catch(e=>{y.warn("upgrade",`Pending reconciliation failed: ${e instanceof Error?e.message:e}`),this.schedulePendingReconcile()})},Ke))}logThrottled(e,t){const r=Date.now();e===this.guardianWaitLoggedPhase&&r-this.guardianWaitLoggedAt<Ye||(this.guardianWaitLoggedPhase=e,this.guardianWaitLoggedAt=r,a(t))}logGuardianWait(e){const t=String(e.phase??"");this.logThrottled(`guardian:${t}`,`transaction ${e.transaction_id??"legacy"} is ${t}; waiting for guardian verdict`)}async resumeWindowsGuardian(){if(this.options.resumeWindowsGuardian){await this.options.resumeWindowsGuardian();return}const e=k();if(!e)return;const{stdio:t,logFd:r}=he();try{x(process.execPath,[e,"activate","--root",b.base],{detached:!0,stdio:t,windowsHide:!0}).unref()}finally{if(r!==void 0)try{H(r)}catch{}}}async retryStartupReport(e,t,r){const n=t?.transaction_id,s=()=>{const u=G();if(!(n&&u?.transaction_id!==n)){if(r&&!B(t)){a(`terminal backup cleanup is still blocked for ${n??"legacy transaction"}`),this.schedulePendingReconcile();return}this.settleTransaction(t)||(a(`terminal shared lock release is still blocked for ${n??"legacy transaction"}`),this.schedulePendingReconcile())}};for(let u=2;u<=C;u++){if(this.stopped)return;const o=oe*(u-1)+Math.floor(Math.random()*1e3);if(await new Promise(i=>setTimeout(i,o)),this.stopped)return;if(await this.reportUpgrade(e)){s();return}}a(`startup report (${e.status}) not delivered after ${C} attempts; clearing pending to unblock future checks`),s()}async confirmHealthz(e,t,r,n){let s="19579";try{const i=v(b.data,"health-port");w(i)&&(s=P(i,"utf-8").trim()||s)}catch{}const u=`http://127.0.0.1:${s}/healthz`,o=Date.now()+e;for(;Date.now()<o&&!this.stopped;){try{const i=await fetch(u,{signal:AbortSignal.timeout(2e3)});if(i.ok){if(!t)return!0;const h=await i.json();if(h.status==="ok"&&h.version===t&&typeof h.pid=="number"&&h.pid>0&&h.pid!==r)try{const d=JSON.parse(P(v(b.base,"daemon-status.json"),"utf-8")),f=Date.parse(n??"");return!!(d.state==="running"&&d.pid===h.pid&&typeof d.updated_at=="number"&&Number.isFinite(f)&&d.updated_at>=f)}catch{return!1}}}catch{}await new Promise(i=>setTimeout(i,je))}return!1}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(e){y.error("upgrade",`Check failed: ${e instanceof Error?e.message:e}`)}finally{this.running=!1}}}async check(){if(K()){await this.handlePendingOnStartup();return}if(this.agentConfigs.length===0)return;const r=(await Promise.all(this.agentConfigs.map(p=>this.queryUpgrade(p)))).find(p=>p.available&&p.release)?.release;if(!r)return;const n=T();let s;try{s=$e(r.version)}catch(p){const _=p;await this.reportUpgrade({from_version:n,to_version:String(r.version),status:"failed",error_code:_.code??"INVALID_VERSION",error_msg:_.message});return}const u=r.force,o=this.options.platform??process.platform,i=this.options.env??process.env,h=te(i);if(h&&this.options.requestGracefulShutdown){await this.runLauncherUpgrade(h,n,s,!!u);return}if(o==="win32"){const p=k(),_=i.GRIX_WRAPPER_SUPERVISED==="1";if(!_||!p||!this.options.requestGracefulShutdown){const S=`Windows automatic upgrade requires a refreshed service installation (${[..._?[]:["managed wrapper"],...p?[]:["external guardian"],...this.options.requestGracefulShutdown?[]:["graceful shutdown hook"]].join(", ")})`;a(S),await this.reportUpgrade({from_version:n,to_version:s,status:"failed",error_code:"WINDOWS_UPGRADE_UNSUPPORTED",error_msg:S});return}}const d=o==="darwin"&&!!k()&&!!this.options.requestGracefulShutdown;if(o==="darwin"&&!d&&a("macOS transactional upgrade prerequisites missing; falling back to the legacy guardian flow"),!u&&!this.checkRateLimit(s))return;const f=X();if(!f.ok){await this.reportUpgrade({from_version:n,to_version:s,status:"failed",error_code:f.errorCode,error_msg:f.errorMsg});return}if(!await j(U,s)){a(`target ${s} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}a(`upgrade start: ${n} -> ${s}`);const l=Date.now();let g;try{g=De(n,s,o==="win32"||d?"preparing":void 0).transaction_id,(await Ae(U,s,void 0,d?{activationMode:"staged"}:{}))?.activationRequired||Pe(s),a("npm install verified, reporting installed"),await this.reportUpgrade({from_version:n,to_version:s,status:"installed",duration_ms:Date.now()-l}),a("shutting down for restart");const _=await this.waitUntilIdle();if(_.outcome!=="idle"){const m=Q("lifecycle-handoff",_);throw a(`upgrade ${m}`),this.options.reopenLifecycleAdmission?.(),new E(ee(_.outcome)?"UPGRADE_DEFERRED":"UPGRADE_ABORTED",m)}if(o==="win32"){if(!k())throw new E("WINDOWS_UPGRADE_UNSUPPORTED","Windows external guardian disappeared before lifecycle handoff");q(process.pid,g),a("Windows handoff armed; requesting graceful daemon shutdown"),await this.options.requestGracefulShutdown("upgrade")}else if(d){if(!k())throw new E("MACOS_UPGRADE_UNSUPPORTED","macOS guardian helper disappeared before lifecycle handoff");q(process.pid,g),a("macOS handoff armed; spawning guardian before graceful daemon shutdown"),await this.resumeWindowsGuardian(),await this.options.requestGracefulShutdown("upgrade")}else this.startGuardian(),process.kill(process.pid,"SIGTERM")}catch(p){const _=p instanceof E?p.code:"NPM_INSTALL_FAILED",m=p instanceof Error?p.message:String(p);a(_==="UPGRADE_DEFERRED"?`upgrade deferred: ${m}`:`upgrade failed: ${_} ${m}`);const S=G();if(g&&S?.transaction_id===g&&(o==="win32"||d?(J(),N(S)):ke(S)&&a("upgrade failed before restart; previous package restored atomically"),z(S)||(a(`failed transaction ${g} is waiting for shared lock release`),this.schedulePendingReconcile()),this.options.reopenLifecycleAdmission?.()),_==="UPGRADE_IN_PROGRESS")return;if(_==="UPGRADE_DEFERRED"){a("machine stayed busy; deferring to the next cycle without recording a failure"),await this.reportUpgrade({from_version:n,to_version:s,status:"deferred",error_msg:m,duration_ms:Date.now()-l,upgrade_log:A()});return}V(m)||Z(m)||_==="NPM_TIMEOUT"?a(`failure is retriable (${_}); skipping cooldown, will retry next cycle`):this.recordFailure(s),await this.reportUpgrade({from_version:n,to_version:s,status:"failed",error_code:_,error_msg:m,duration_ms:Date.now()-l,upgrade_log:A()})}}async runLauncherUpgrade(e,t,r,n){if(!n&&!this.checkRateLimit(r))return;const s=X();if(!s.ok){await this.reportUpgrade({from_version:t,to_version:r,status:"failed",error_code:s.errorCode,error_msg:s.errorMsg});return}if(!await j(U,r)){a(`target ${r} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}a(`launcher upgrade start: ${t} -> ${r} (store=${e})`);const u=Date.now();try{w(D(e,r))?a(`${r} already present in version store; skipping install`):await Ie(e,U,r),await this.reportUpgrade({from_version:t,to_version:r,status:"installed",duration_ms:Date.now()-u});const o=await this.waitUntilIdle();if(o.outcome!=="idle"){const h=Q("launcher-switch handoff",o);throw a(`launcher upgrade ${h}`),this.options.reopenLifecycleAdmission?.(),new E(ee(o.outcome)?"UPGRADE_DEFERRED":"UPGRADE_ABORTED",h)}const i={transaction_id:He(),from_version:t,to_version:r,started_at:new Date().toISOString()};ae(e,{...i,switched:!1}),We(e,r),ae(e,{...i,switched:!0}),a(`current pointer switched to ${r}; requesting restart via launcher`),await this.options.requestGracefulShutdown("upgrade-switch")}catch(o){const i=o instanceof E?o.code:"NPM_INSTALL_FAILED",h=o instanceof Error?o.message:String(o);if(a(i==="UPGRADE_DEFERRED"?`launcher upgrade deferred: ${h}`:`launcher upgrade failed: ${i} ${h}`),i==="UPGRADE_DEFERRED"||i==="UPGRADE_ABORTED"){const f=i==="UPGRADE_DEFERRED";a(f?"machine stayed busy; keeping the verified version dir and retrying next cycle without penalty":"connector stopped before the switch; keeping the verified version dir for the next start"),await this.reportUpgrade({from_version:t,to_version:r,status:f?"deferred":"failed",error_code:f?void 0:i,error_msg:h,duration_ms:Date.now()-u,upgrade_log:A()});return}if($(e,"current")!==r)try{R(ie(e,r),{recursive:!0,force:!0})}catch{}V(h)||Z(h)||i==="NPM_TIMEOUT"?a(`failure is retriable (${i}); skipping cooldown, will retry next cycle`):this.recordFailure(r),await this.reportUpgrade({from_version:t,to_version:r,status:"failed",error_code:i,error_msg:h,duration_ms:Date.now()-u,upgrade_log:A()})}}async handleLauncherStartup(){const e=this.options.env??process.env,t=te(e);if(!t)return;const r=T();try{Ne(t,ie(t,r))}catch(l){a(`failed to refresh launcher: ${l instanceof Error?l.message:String(l)}`)}const n=Date.now(),s=()=>{if(w(Ge(t,r)))return!1;const l=$(t,"previous");return!!l&&l!==r&&w(D(t,l))},u=l=>{this.healthyTimer=setTimeout(()=>{if(this.healthyTimer=null,!this.stopped){if(this.options.isConnected&&!this.options.isConnected()){if(Date.now()-n>=et&&this.options.requestGracefulShutdown&&s()){a(`${r} has been up ${Math.round((Date.now()-n)/1e3)}s without any agent websocket ready; exiting so the launcher counts a crash and can roll back`),this.options.requestGracefulShutdown("launcher-unhealthy");return}a(`uptime reached but no agent websocket is ready yet; deferring health marker for ${r}`),u(Qe);return}try{Me(t,r),re(t,r)}catch(g){a(`failed to write health marker: ${g instanceof Error?g.message:String(g)}`)}}},l),this.healthyTimer.unref?.()};u(Ze);const o=Oe(t),i=Ce(t);if(!o&&!i)return;const h=()=>{try{R(L(t),{force:!0})}catch{}try{R(O(t),{force:!0})}catch{}const l=Le(t,e);l.length>0&&a(`pruned version directories: ${l.join(", ")}`)},d=()=>{const l=_=>{const m=_?Date.parse(_):Number.NaN;return Number.isFinite(m)&&Date.now()-m<tt},g=!!(o&&o.from_version!==r&&w(D(t,o.from_version))&&l(o.rolled_back_at)),p=!!(i&&i.to_version!==r&&i.from_version!==r&&w(D(t,i.from_version))&&l(i.started_at));if(!g)try{R(L(t),{force:!0})}catch{}if(!p)try{R(O(t),{force:!0})}catch{}(g||p)&&a(`kept launcher records addressed to another installed version (rollback=${g} switch=${p})`)};let f;if(o&&o.from_version===r){const l=`exit_code=${o.exit_code??"n/a"} crashes=${o.crash_count??"n/a"} uptime_ms=${o.uptime_ms??"n/a"}`;a(`daemon startup: launcher rolled back ${o.to_version} -> ${r} (${l})`),this.recordFailure(o.to_version);const g=re(t,o.to_version),p=g?.split(/\r?\n/)[0]?.slice(0,200);f={from_version:o.from_version,to_version:o.to_version,status:"rolled_back",error_code:"STARTUP_CRASH",error_msg:`Target ${o.to_version} crashed before becoming healthy (${l}); launcher restored ${r}`+(p?`; cause: ${p}`:""),crash_count:o.crash_count,upgrade_log:A()+(g?`
|
|
2
2
|
--- ${o.to_version} crash note ---
|
|
3
|
-
${
|
|
3
|
+
${g}`:"")}}else if(i&&i.to_version===r&&$(t,"current")===r){a(`daemon startup: running switched target ${r}, upgrade succeeded`);const l=await this.confirmHealthz(ce);a(l?"healthz confirmed healthy, reporting success":"healthz not confirmed within window; reporting success on version-match"),f={from_version:i.from_version,to_version:i.to_version,status:"success"}}else if(i&&i.from_version===r&&i.switched===!1){a(`daemon startup: switch record for ${i.to_version} was never applied; discarding`);try{R(O(t),{force:!0})}catch{}try{R(L(t),{force:!0})}catch{}return}else if(i&&i.from_version===r)a(`daemon startup: expected ${i.to_version} but ${r} is running; reporting rolled_back`),this.recordFailure(i.to_version),f={from_version:i.from_version,to_version:i.to_version,status:"rolled_back",error_code:"STARTUP_CRASH",error_msg:`Launcher is running ${r} instead of switched target ${i.to_version}`};else{a(`daemon startup: launcher records do not match this process (self=${r} current=${$(t,"current")??"<none>"} previous=${$(t,"previous")??"<none>"} switch=${i?`${i.from_version}->${i.to_version}/switched=${i.switched!==!1}`:"<none>"} rollback=${o?`${o.to_version}->${o.from_version}`:"<none>"}); discarding records without pruning`),d();return}if(await this.reportUpgrade(f)){h();return}(async()=>{for(let l=2;l<=C;l++){if(this.stopped||(await new Promise(g=>setTimeout(g,oe*(l-1)+Math.floor(Math.random()*1e3))),this.stopped))return;if(await this.reportUpgrade(f))break}h()})()}async waitUntilIdle(){return Ue({describeBusy:()=>this.describeBusy?.()??[],isStopped:()=>this.stopped,closeAdmission:()=>this.closeAdmission?.(),reopenAdmission:()=>this.options.reopenLifecycleAdmission?.(),budgetMs:it,log:(e,t)=>{a(t),e==="warn"&&y.warn("upgrade",t)}})}checkRateLimit(e){const t=_e();if(t.last_failure_at){const u=Date.now()-new Date(t.last_failure_at).getTime();if(u<ne)return y.info("upgrade",`In cooldown, ${(ne-u)/6e4}m remaining`),!1}const r=t.version_attempts[e]??0;if(r>=Be)return y.info("upgrade",`Version ${e} already tried ${r} times, skipping`),!1;const n=me(),s=t.daily_attempts[n]??0;return s>=qe?(y.info("upgrade",`Already ${s} attempts today, skipping`),!1):!0}recordFailure(e){const t=_e(),r=me();t.last_failure_at=new Date().toISOString(),t.last_failure_version=e,t.daily_attempts[r]=(t.daily_attempts[r]??0)+1,t.version_attempts[e]=(t.version_attempts[e]??0)+1;for(const s of Object.keys(t.daily_attempts))s<r&&delete t.daily_attempts[s];const n=Object.keys(t.version_attempts);for(const s of n.slice(0,Math.max(0,n.length-ze)))s!==e&&delete t.version_attempts[s];st(t)}async queryUpgrade(e){const r=`${fe(e.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:T(),channel:e.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const n=await fetch(r,{headers:{Authorization:`Bearer ${e.apiKey}`},signal:AbortSignal.timeout(se)});if(!n.ok)return y.warn("upgrade",`Check API returned ${n.status}`),{available:!1};const s=await n.json();return s.code!==0?{available:!1}:s.data}catch(n){return y.warn("upgrade",`Check API error: ${n instanceof Error?n.message:n}`),{available:!1}}}async reportUpgrade(e){if(this.agentConfigs.length===0)return!1;const t={client_type:"grix-connector",...e.npm_version?e:{...e,...Te()}};return(await Promise.all(this.agentConfigs.map(async n=>{const u=`${fe(n.wsUrl)}/v1/agent-api/upgrade/report`;try{return(await fetch(u,{method:"POST",headers:{Authorization:`Bearer ${n.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:AbortSignal.timeout(se)})).ok}catch{return!1}}))).some(n=>n)}startGuardian(){const e=v(b.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!w(e)){y.info("upgrade","Guardian not available on this platform, skipping");return}const{stdio:t,logFd:r}=he();try{const n=x(e,[],{detached:!0,stdio:t});n.unref(),a(`guardian started (pid ${n.pid})`)}catch(n){y.warn("upgrade",`Failed to start guardian: ${n instanceof Error?n.message:n}`)}finally{if(r!==void 0)try{H(r)}catch{}}}}export{St as UpgradeChecker};
|
package/dist/grix.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import b from"node:path";import{writeFileSync as ae}from"node:fs";import{Manager as
|
|
3
|
-
Run \`grix-connector --help\` to see the supported commands and options.`),process.exit(1)),
|
|
2
|
+
import b from"node:path";import{writeFileSync as ae}from"node:fs";import{Manager as Ce}from"./manager.js";import{ensureGrixDirs as Te,initLogger as $e,log as i,installProcessLogRotation as Le,setConsoleOutput as ie}from"./core/log/index.js";import{HealthServer as Ue,bindPortOrFail as se}from"./core/runtime/index.js";import{writePidFile as He,removePidFile as T,readDaemonPid as ce}from"./core/runtime/index.js";import{killOrphanedClaudeProcesses as Oe}from"./core/runtime/index.js";import{resolveRuntimePaths as V}from"./core/config/index.js";import{supportsNonMitmRelayConfig as Fe}from"./core/config/provider-env.js";import{ServiceManager as le}from"./service/service-manager.js";import{RESTART_EXIT_CODE as Me,UNHEALTHY_EXIT_CODE as Ne,markManagedStop as de,resolveLauncherStore as Y,writeCrashNote as Ge}from"./core/upgrade/version-store.js";import{migrateWindowsWrapper as q}from"./service/windows-wrapper-migration.js";const Be=300*1e3;import{killProcessesByCommandLine as je,isWindowsElevated as We,shouldSweepWindowsWrappers as Ke,getProcessCommandLine as Xe}from"./service/process-control.js";import{buildServiceID as Ve}from"./service/service-paths.js";import{isInsideManagedDaemonTree as Ye}from"./service/platform-adapter.js";import{acquireDaemonLock as qe,createHealthzHolderProbe as Je,DaemonAlreadyServingError as ze,findLiveLockHolder as Qe,isLockHolderSameProcess as Ze,readDaemonLock as J,releaseDaemonLock as F}from"./runtime/daemon-lock.js";import{writeDaemonStatus as G,removeDaemonStatus as et}from"./runtime/service-state.js";import{readDuplicateDaemonRecord as tt,recordDuplicateDaemon as rt}from"./runtime/duplicate-daemon.js";import{RemoteAdminSettingsStore as nt}from"./core/config/remote-admin-settings.js";import{AdminServer as ot,generateToken as at,writeTokenFile as it}from"./core/admin/index.js";import{initSentry as st,closeSentry as B,reportFatal as z}from"./core/observability/sentry.js";import{initProxyManager as ct,getProxyManager as f,relayHostsForClientType as pe,RelayStateStore as lt,SecretStore as dt}from"./core/proxy/index.js";import{stopProxyIfNobodyNeedsIt as me,switchAgentRelayMode as Q,reconcileRelayStates as ge}from"./core/proxy/relay-orchestration.js";import{setAgentRelayCredential as ue,RelayCredentialError as fe}from"./core/proxy/relay-credential.js";import{RelayFetchError as he,RelayToggleCoordinator as pt}from"./core/proxy/relay-credential-fetch.js";import{applyHermesProfileRelay as mt,clearHermesProfileRelay as gt,discoverHermesProfiles as ut}from"./core/proxy/hermes-profile-relay.js";import{disableRelayCredential as ft,enableRelayCredential as ht,RelayRouteError as ye}from"./core/proxy/relay-credential-router.js";import{upgradeStatusSnapshot as yt}from"./core/upgrade/npm-upgrader.js";import{resolveClientVersion as wt}from"./core/util/client-version.js";import{parseCliArgs as St}from"./core/util/cli-args.js";const{command:I,flags:D,unknownFlags:Z}=St(process.argv.slice(2));if(Z.length>0&&(console.error(`Unknown option${Z.length>1?"s":""}: ${Z.map(t=>`--${t}`).join(", ")}
|
|
3
|
+
Run \`grix-connector --help\` to see the supported commands and options.`),process.exit(1)),D.version&&(console.log(wt()),process.exit(0)),D.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
|
|
4
4
|
|
|
5
5
|
Usage: grix-connector <command> [options]
|
|
6
6
|
|
|
@@ -34,10 +34,10 @@ Examples:
|
|
|
34
34
|
grix-connector start # Start as system service
|
|
35
35
|
grix-connector status # Check service status
|
|
36
36
|
grix-connector restart # Restart the service
|
|
37
|
-
`),process.exit(0)),I==="reload"){const t=
|
|
37
|
+
`),process.exit(0)),I==="reload"){const t=ce();t||(console.error("reload failed: daemon is not running (no pid file)"),process.exit(1));try{process.kill(t,0)}catch{console.error(`reload failed: daemon process ${t} is not running (stale pid file)`),process.exit(1)}{const s=J(V().daemonLockFile);s&&s.pid===t&&!Ze(s)&&(console.error(`reload failed: pid ${t} has been reused by another process (stale pid file)`),process.exit(1))}try{process.kill(t,"SIGHUP"),console.log(JSON.stringify({ok:!0,signaled:t},null,2)),process.exit(0)}catch(s){console.error(`reload failed: ${s instanceof Error?s.message:s}`),process.exit(1)}}const we="detected connector-managed session; restart delegated to launchd, this process may be terminated by the restart",Se=["start","stop","restart","status"];if(I&&Se.includes(I)){process.platform==="win32"&&["start","stop","restart"].includes(I)&&!We()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
|
|
38
38
|
using Startup folder auto-start instead. For full Task Scheduler integration,
|
|
39
|
-
right-click the terminal and select "Run as administrator".`);const t=
|
|
40
|
-
Valid commands: ${
|
|
41
|
-
`),i.warn("main",
|
|
42
|
-
`),i.error("main",t.message.replace(/\n/g," \u2014 ")),j("port_bind",t.message);const l=Y(c.daemonLockFile);l?.pid===process.pid&&l.instance_id===T&&await G(c.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:T,updated_at:Date.now(),reason:`port_bind_${t.kind}:${t.label}:${t.port}`}).catch(()=>{}),await J(),F(c.daemonLockFile).catch(()=>{}),H(),process.exit(1)}async function _(t){if(B)return;B=!0,i.info("main",`Received ${t}, shutting down...`),M.markShuttingDown();const l=V();l&&le(l);const u=t==="upgrade-switch"?Fe:t==="launcher-unhealthy"?Me:null,m=u??2,s=t==="uncaughtException"||t==="unhandledRejection",h=setTimeout(()=>{i.error("main","Shutdown timed out, forcing exit"),F(c.daemonLockFile).catch(()=>{}),H(),process.exit(m)},1e4);try{await o.stop(),await f()?.stop().catch(()=>{}),await x.stop(),await M.stop(),await J(),await F(c.daemonLockFile),await Qe(c.daemonStatusFile).catch(()=>{}),clearTimeout(h),H(),i.info("main","Shutdown complete"),process.exit(u??(s?2:0))}catch(S){i.error("main",`Shutdown error: ${S}`),F(c.daemonLockFile).catch(()=>{}),H(),process.exit(m)}}async function Rt(){ke(),Ce(),Te(c.stdoutLogFile,c.stderrLogFile),ie(!1);try{T=(await qe(c.daemonLockFile,c.rootDir,2,{isHolderServing:Ye(c.dataDir)})).instance_id}catch(e){e instanceof Je&&St(e.holder),console.error(e instanceof Error?e.message:e),process.exit(1)}if(We()){const e=Xe(c.rootDir,"win32");await Be(e,{platform:"win32"})}await at(),Le(),i.info("main",`grix-connector starting (PID ${process.pid})`);try{Oe({info:e=>i.info("main",e),warn:e=>i.warn("main",e)})}catch(e){i.warn("main",`orphan Claude cleanup failed: ${e instanceof Error?e.message:String(e)}`)}if(await G(c.daemonStatusFile,{state:"starting",pid:process.pid,instance_id:T,updated_at:Date.now()}),process.env.GRIX_LAUNCHER_MANAGED!=="1"&&await new ce({cliPath:b.resolve(process.argv[1]??""),nodePath:process.execPath}).adoptLauncher(c.rootDir).then(e=>{e&&i.info("main","Service definition now points at the version-store launcher")}).catch(e=>{i.warn("main",`Launcher adoption failed: ${e instanceof Error?e.message:e}`)}),(await q({rootDir:c.rootDir,requestGracefulShutdown:()=>{_("wrapper-migration")}}).catch(e=>(i.warn("main",`Windows wrapper migration failed: ${e instanceof Error?e.message:e}`),null)))?.handoffStarted)return;process.platform==="win32"&&process.env.GRIX_LAUNCHER_MANAGED!=="1"&&setInterval(()=>{B||q({rootDir:c.rootDir,requestGracefulShutdown:()=>{_("wrapper-migration")}}).then(n=>{n.handoffStarted&&i.info("main","Wrapper handoff started by the periodic migration retry")}).catch(n=>{i.warn("main",`Periodic wrapper migration failed: ${n instanceof Error?n.message:n}`)})},Ge).unref();const l=parseInt(A["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await se({label:"health",port:l,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:n=>M.start(n)});e&&await Re(e)}const u=b.join(c.dataDir,"health-port");ae(u,String(l),"utf-8"),process.on("SIGINT",()=>_("SIGINT")),process.on("SIGTERM",()=>_("SIGTERM")),process.on("SIGHUP",()=>{B||(i.info("main","Received SIGHUP, reloading config..."),o.reload().then(e=>i.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>i.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let m="",s=0,h;process.on("uncaughtException",e=>{const n=e instanceof Error?e.stack??e.message:String(e);n===m?(s++,(s<=3||s%100===0)&&i.error("main",`Uncaught exception (x${s}): ${n}`)):(s>3&&i.error("main",`Previous exception repeated ${s} times total`),m=n,s=1,i.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),h||(h=setTimeout(()=>{s>3&&i.error("main",`Previous exception repeated ${s} times total`),m="",s=0,h=void 0},1e4).unref())),!Ee(e)&&(z(e,"uncaughtException"),j("uncaughtException",e),_("uncaughtException"))}),process.on("unhandledRejection",e=>{i.error("main",`Unhandled rejection: ${e}`),!Ee(e)&&(z(e,"unhandledRejection"),j("unhandledRejection",e),_("unhandledRejection"))});const S=it(c.dataDir);await S.clearRuntimeState();const k=new st(b.join(c.dataDir,"proxy","relay-state.json"),{warn:e=>i.warn("main",e)}),N=new ct(b.join(c.dataDir,"secrets"));o.setRelaySecretResolver(e=>N.get(e));const $={hasDirectConfig:e=>o.hasDirectRelayConfig(e),clearDirectConfig:(e,n)=>o.setAgentProvider(e,void 0,{deferRestart:n?.deferRestart===!0})},W=()=>({pm:f(),agents:o,direct:$,store:k,secrets:N}),L=S.getRelayAgents();if(L.length>0)try{await S.startIfAnyRelayAgent(),i.info("main",`mitm proxy started for relay agents: ${L.join(", ")} (per-agent; others stay direct)`)}catch(e){i.error("main",`mitm proxy FAILED to start (relay agents: ${L.join(", ")}): ${e}. These agents will REFUSE to start (they must not silently fall back to your own account). Fix the proxy or turn relay off for them. Other agents are unaffected.`)}else i.info("main",'mitm proxy idle (no agent has Grix relay enabled; enable: PUT /api/proxy/agents/<name>/enabled {"enabled":true})');let U=null;if(f()){const e=async()=>{const r=f();r&&await pe(r,o)&&i.info("main","no agent uses Grix relay anymore; mitm proxy stopped")};o.setRelayEnvSettledHandler(()=>{const r=f();r&&ge(r,$,k),e().catch(a=>{i.warn("main",`failed to stop idle mitm proxy: ${a}`)})});const n=()=>{const r=f(),a=r.getRelayAgents(),d=r.getDegradedRelayAgents();return{enabled:a.length>0,relayAgents:a,staleRelayAgents:o.getAgentsWithStaleRelayEnv(),degradedRelayAgents:d,relayState:k.list(),runtime:r.getRuntimeInfo(),config:r.getConfigSnapshot()}},p=new lt,D={status:()=>n(),setAgentRelay:async(r,a,d)=>{const g=f(),E={info:w=>i.info("main",w),warn:w=>i.warn("main",w)},R=w=>o.getAgentsStatus().find(v=>v.name===w),y=R(r);if(!a){p.cancel(r);const w=await p.runExclusive(r,async()=>{const v=await Q(W(),E,{agentName:r,target:"off"});return{...n(),restarted:v.restarted,busy:v.busy,...v.pending?{pending:v.pending}:{}}});return o.reportRelayStateLocalChange(r),w}if(!y)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});if(!He(y.clientType)&&de(y.clientType).length===0)throw Object.assign(new Error(`client type "${y.clientType??""}" does not support Grix relay`),{code:"UNSUPPORTED_CLIENT_TYPE"});const O=await p.runExclusive(r,async()=>{const w=p.epoch(r),v=R(r);if(!v)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});const re=String(v.agentId??"").trim();if(!re)throw new ue(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const ne=p.waitCancelled(r,w);let C;try{C=await Promise.race([o.fetchRelayCredential(r,{model:d}),ne.promise.then(()=>{throw new fe("relay enable cancelled by a later disable","CANCELLED")})])}finally{ne.dispose()}p.assertCurrent(r,w);const{restarted:ve,busy:be,pending:oe}=await me(g,o,R,E,{localName:r,agentId:re,virtualKey:C.apiKey,anthropicBaseUrl:C.anthropicBaseUrl,openaiBaseUrl:C.openaiBaseUrl,model:d,...C.directRelay!==void 0?{directRelay:C.directRelay}:{}},(xe,Pe,Ie)=>o.setAgentProvider(xe,Pe,{deferRestart:Ie?.deferRestart===!0}),{store:k,secrets:N,direct:$});if(p.epoch(r)!==w)throw await Q(W(),E,{agentName:r,target:"off"}),new fe("relay enable cancelled by a later disable","CANCELLED");return{...n(),restarted:ve,busy:be,...oe?{pending:oe}:{}}});return o.reportRelayStateLocalChange(r),O},setAgentRelayCredential:async(r,a)=>{const d=f(),g=o.getAgentsStatus(),{restarted:E,busy:R,pending:y}=await me(d,o,P=>g.find(O=>O.name===P),{info:P=>i.info("main",P),warn:P=>i.warn("main",P)},{localName:r,agentId:a.agentId,virtualKey:a.virtualKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}},(P,O,w)=>o.setAgentProvider(P,O,{deferRestart:w?.deferRestart===!0}),{store:k,secrets:N,direct:$});return o.reportRelayStateLocalChange(r),{...n(),restarted:E,busy:R,...y?{pending:y}:{}}},disableAll:async()=>{const r=f(),a={info:g=>i.info("main",g),warn:g=>i.warn("main",g)},d=new Set([...r.getRelayAgents(),...o.getDirectRelayConfigAgentNames(),...Object.keys(k.list())]);for(const g of d)p.cancel(g);for(const g of d)await p.runExclusive(g,()=>Q(W(),a,{agentName:g,target:"off"})),o.reportRelayStateLocalChange(g);return await pe(r,o),n()},setRoute:async(r,a)=>{const d=a,g=f();return g.setRoute({routeKey:r,targetBaseUrl:d.targetBaseUrl,...d.headers?{headers:d.headers}:{},...d.model?{model:d.model}:{},...d.modelMap?{modelMap:d.modelMap}:{},...d.passthrough?{passthrough:!0}:{},...d.relayOnly?{relayOnly:!0}:{},...d.codexResponsesToChat?{codexResponsesToChat:!0}:{}}),await g.persistConfig(),n()},deleteRoute:async r=>{const a=f();a.deleteRoute(r),await a.persistConfig()},setDefaultRoute:async r=>{const a=f();return a.setDefaultRouteKey(r),await a.persistConfig(),n()},setInterceptHosts:async r=>{const a=f();return a.setInterceptHosts(r),await a.persistConfig(),n()}};U=D,x.setProxyHandler(D),o.setRelayStateApplier({getLocalState:r=>{const a=f();if(a.getRelayAgents().includes(r)){const g=o.getAgentsStatus().find(y=>y.name===r),E=`grix-gateway-${String(g?.clientType??"").trim().toLowerCase()}`;return{enabled:!0,model:a.getConfigSnapshot().routes?.find(y=>y.routeKey===E)?.model?.trim()||void 0}}const d=o.getAgentProvider(r);return d?.api_key?.trim()?{enabled:!0,model:d.model?.trim()||void 0}:{enabled:!1}},applyEnable:async(r,a,d)=>{const g=o.getAgentsStatus().find(y=>y.name===r),E=String(g?.agentId??"").trim();if(!E)throw new ue(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const R=await D.setAgentRelayCredential(r,{agentId:E,virtualKey:a.apiKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:d??a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}});return{restarted:R.restarted===!0,busy:R.busy===!0,...R.pending?{pending:R.pending}:{}}},applyDisable:async r=>{const a=await D.setAgentRelay(r,!1);return{restarted:a.restarted===!0,busy:a.busy===!0,...a.pending?{pending:a.pending}:{}}}})}M.setStatusProvider(()=>o.getAgentsStatus()),M.setMetaProvider(()=>{const e=o.getAgentsStatus();return{ws:{connected:e.filter(n=>n.wsConnected===!0).length,total:e.length},upgrade:ft()}});const K=new tt(b.join(c.dataDir,"remote-admin.json"));if(o.setRemoteAdminGate(()=>K.isEnabled()),await o.start(wt),L.length>0){const e=o.getAgentsStatus();for(const n of L){const p=e.find(r=>r.name===n);if(!p)continue;const D=de(p.clientType);D.length!==0&&await S.setAgentRelayEnabled(n,!0,{relayHosts:D}).catch(r=>{i.warn("main",`failed to reconcile relay hosts for "${n}": ${r}`)})}}ge(S,$,k);const ee=parseInt(A["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);x.setAgentHandler({list:()=>o.getAgentsStatus(),add:e=>o.addAgent(e),remove:e=>o.removeAgent(e),restart:e=>o.restartAgent(e),reload:()=>o.reload()}),x.setUpgradeHandler({check:()=>o.checkUpgrade(),trigger:()=>o.triggerUpgrade()}),x.setProbeHandler({probeAll:e=>o.probeAll(e),probeOne:(e,n)=>o.probeOne(e,n)}),x.setRemoteAdminHandler({isEnabled:()=>K.isEnabled(),setEnabled:e=>{const n=K.setEnabled(e);return i.info("main",`remote admin ${n?"enabled":"disabled"} via admin API`),n}}),x.setInstallHandler({listInstallable:()=>o.listInstallable(),installAgent:e=>o.installAgent(e),getInstallProgress:e=>o.getInstallProgress(e)});const te={listAgents:()=>o.getAgentsStatus(),enableManaged:(e,n,p)=>{if(!U)throw new he(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return U.setAgentRelayCredential(e,{agentId:n,virtualKey:p.virtualKey,anthropicBaseUrl:p.anthropicBaseUrl,openaiBaseUrl:p.openaiBaseUrl,model:p.model,...p.directRelay!==void 0?{directRelay:p.directRelay}:{}})},disableManaged:e=>{if(!U)throw new he(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return U.setAgentRelay(e,!1)},enableHermesProfile:(e,n)=>dt({agentId:e,baseUrl:n.openaiBaseUrl??"",virtualKey:n.virtualKey,model:n.model??""}),disableHermesProfile:e=>pt(e)};x.setRelayCredentialHandler({listHermesProfiles:()=>gt(),enable:async(e,n)=>{const{target:p,result:D}=await ut(te,e,n);return{target:p,...D}},disable:async e=>{const{target:n,result:p}=await mt(te,e);return{target:n,...p}}});{const e=await se({label:"admin",port:ee,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:n=>x.start(n)});e&&await Re(e)}const Ae=b.join(c.dataDir,"admin-token"),De=b.join(c.dataDir,"admin-port");ot(Ae,Se),ae(De,String(ee),"utf-8"),await G(c.daemonStatusFile,{state:"running",pid:process.pid,instance_id:T,updated_at:Date.now()}),process.send&&process.send("ready"),i.info("main","grix-connector ready")}Rt().catch(async t=>{const l=`Fatal: ${t instanceof Error?t.stack??t.message:String(t)}`;try{process.stderr.write(`${l}
|
|
43
|
-
`)}catch{}ie(!1),i.error("main",
|
|
39
|
+
right-click the terminal and select "Run as administrator".`);const t=V(),s=D["config-dir"]??(D.profile?b.join(t.configDir,D.profile):void 0),m=b.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),u=new le({cliPath:m,nodePath:process.execPath});try{let l;switch(I){case"start":{const h=await u.healLegacyState({rootDir:t.rootDir});(await u.status({rootDir:t.rootDir})).installed?l=await u.start({rootDir:t.rootDir}):l=await u.install({rootDir:t.rootDir,configDir:s??h.configDir}),h.healed&&(l={...l,legacy_heal:h.reasons})}break;case"stop":l=await u.stop({rootDir:t.rootDir});break;case"restart":if(!(await u.status({rootDir:t.rootDir})).installed)l=await u.install({rootDir:t.rootDir,configDir:s});else{const S=process.platform==="darwin"&&Ye();S&&console.warn(we),l=await u.restart({rootDir:t.rootDir}),S&&(l={...l,mode:"kickstart",note:we})}break;case"status":l=await u.status({rootDir:t.rootDir});{const h=tt(t.dataDir);h&&(l={...l,duplicate_daemon:h})}break}console.log(JSON.stringify(l,null,2)),process.exit(0)}catch(l){console.error(`${I} failed: ${l instanceof Error?l.message:l}`),process.exit(1)}}else I&&(console.error(`Unknown command: ${I}
|
|
40
|
+
Valid commands: ${Se.join(", ")}`),process.exit(1));const c=V(),Rt=D["config-dir"]??(D.profile?`${c.configDir}/${D.profile}`:void 0),o=new Ce({requestGracefulShutdown:t=>{k(t)},onTransactionSettled:()=>{q({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).then(t=>{t.handoffStarted&&i.info("main","Wrapper handoff started after the upgrade transaction settled")}).catch(t=>{i.warn("main",`Post-upgrade wrapper migration failed: ${t instanceof Error?t.message:t}`)})}}),M=new Ue,Re=at(),x=new ot(Re);let j=!1,$;function W(t,s){const m=Y(),u=process.env.GRIX_LAUNCHER_VERSION;if(!m||!u)return;const l=s instanceof Error?s.stack??s.message:String(s);Ge(m,u,`${t}: ${l}`)}function Ee(t,s){rt(c.dataDir,{holder_pid:t.pid,holder_command_line:t.command_line,yielded_pid:process.pid,yielded_command_line:Xe(process.pid)}),process.stderr.write(`${s}
|
|
41
|
+
`),i.warn("main",s);const m=Y();m&&de(m)}function Et(t){const s=`another grix-connector daemon (pid ${t.pid}) is already serving ${c.rootDir} and answering /healthz; this process yields instead of taking over. A second supervisor is starting duplicate daemons \u2014 check for a leftover wrapper, a duplicate logon task, or a desktop app launching its own copy.`;Ee(t,s),process.exit(0)}function Dt(t,s){const m=`${s.label} \u7AEF\u53E3 ${s.port} \u5DF2\u88AB\u5360\u7528\uFF0C\u4E14 daemon \u9501\u4ECD\u7531\u53E6\u4E00\u4E2A\u5B58\u6D3B\u5B9E\u4F8B (pid ${t.pid}) \u6301\u6709\uFF1A\u672C\u8FDB\u7A0B\u6309\u7BA1\u7406\u6027\u9000\u51FA\u8BA9\u4F4D\uFF0C\u4E0D\u8BA1\u4E3A\u5D29\u6E83\u3002 \u8FD9\u53F0\u673A\u5668\u4E0A\u6709\u7B2C\u4E8C\u4E2A supervisor \u5728\u91CD\u590D\u62C9\u8D77 daemon\u2014\u2014\u68C0\u67E5\u6B8B\u7559\u7684 wrapper\u3001\u91CD\u590D\u7684\u767B\u5F55\u4EFB\u52A1\uFF0C\u6216\u684C\u9762\u7AEF\u53E6\u8D77\u7684\u526F\u672C\u3002`;Ee(t,m),ce()===process.pid&&T(),process.exit(0)}async function De(t){if(t.kind==="in_use"){const m=Qe(c.daemonLockFile);m&&(await B(),Dt(m,t))}process.stderr.write(t.message+`
|
|
42
|
+
`),i.error("main",t.message.replace(/\n/g," \u2014 ")),W("port_bind",t.message);const s=J(c.daemonLockFile);s?.pid===process.pid&&s.instance_id===$&&await G(c.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:$,updated_at:Date.now(),reason:`port_bind_${t.kind}:${t.label}:${t.port}`}).catch(()=>{}),await B(),F(c.daemonLockFile).catch(()=>{}),T(),process.exit(1)}async function k(t){if(j)return;j=!0,i.info("main",`Received ${t}, shutting down...`),M.markShuttingDown();const s=Y();s&&de(s);const m=t==="upgrade-switch"?Me:t==="launcher-unhealthy"?Ne:null,u=m??2,l=t==="uncaughtException"||t==="unhandledRejection",h=setTimeout(()=>{i.error("main","Shutdown timed out, forcing exit"),F(c.daemonLockFile).catch(()=>{}),T(),process.exit(u)},1e4);try{await o.stop(),await f()?.stop().catch(()=>{}),await x.stop(),await M.stop(),await B(),await F(c.daemonLockFile),await et(c.daemonStatusFile).catch(()=>{}),clearTimeout(h),T(),i.info("main","Shutdown complete"),process.exit(m??(l?2:0))}catch(S){i.error("main",`Shutdown error: ${S}`),F(c.daemonLockFile).catch(()=>{}),T(),process.exit(u)}}async function At(){Te(),$e(),Le(c.stdoutLogFile,c.stderrLogFile),ie(!1);try{$=(await qe(c.daemonLockFile,c.rootDir,2,{isHolderServing:Je(c.dataDir)})).instance_id}catch(e){e instanceof ze&&Et(e.holder),console.error(e instanceof Error?e.message:e),process.exit(1)}if(Ke()){const e=Ve(c.rootDir,"win32");await je(e,{platform:"win32"})}await st(),He(),i.info("main",`grix-connector starting (PID ${process.pid})`);try{Oe({info:e=>i.info("main",e),warn:e=>i.warn("main",e)})}catch(e){i.warn("main",`orphan Claude cleanup failed: ${e instanceof Error?e.message:String(e)}`)}if(await G(c.daemonStatusFile,{state:"starting",pid:process.pid,instance_id:$,updated_at:Date.now()}),process.env.GRIX_LAUNCHER_MANAGED!=="1"&&await new le({cliPath:b.resolve(process.argv[1]??""),nodePath:process.execPath}).adoptLauncher(c.rootDir).then(e=>{e&&i.info("main","Service definition now points at the version-store launcher")}).catch(e=>{i.warn("main",`Launcher adoption failed: ${e instanceof Error?e.message:e}`)}),(await q({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).catch(e=>(i.warn("main",`Windows wrapper migration failed: ${e instanceof Error?e.message:e}`),null)))?.handoffStarted)return;process.platform==="win32"&&process.env.GRIX_LAUNCHER_MANAGED!=="1"&&setInterval(()=>{j||q({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).then(n=>{n.handoffStarted&&i.info("main","Wrapper handoff started by the periodic migration retry")}).catch(n=>{i.warn("main",`Periodic wrapper migration failed: ${n instanceof Error?n.message:n}`)})},Be).unref();const s=parseInt(D["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await se({label:"health",port:s,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:n=>M.start(n)});e&&await De(e)}const m=b.join(c.dataDir,"health-port");ae(m,String(s),"utf-8"),process.on("SIGINT",()=>k("SIGINT")),process.on("SIGTERM",()=>k("SIGTERM")),process.on("SIGHUP",()=>{j||(i.info("main","Received SIGHUP, reloading config..."),o.reload().then(e=>i.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>i.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let u="",l=0,h;process.on("uncaughtException",e=>{const n=e instanceof Error?e.stack??e.message:String(e);n===u?(l++,(l<=3||l%100===0)&&i.error("main",`Uncaught exception (x${l}): ${n}`)):(l>3&&i.error("main",`Previous exception repeated ${l} times total`),u=n,l=1,i.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),h||(h=setTimeout(()=>{l>3&&i.error("main",`Previous exception repeated ${l} times total`),u="",l=0,h=void 0},1e4).unref())),!Ae(e)&&(z(e,"uncaughtException"),W("uncaughtException",e),k("uncaughtException"))}),process.on("unhandledRejection",e=>{i.error("main",`Unhandled rejection: ${e}`),!Ae(e)&&(z(e,"unhandledRejection"),W("unhandledRejection",e),k("unhandledRejection"))});const S=ct(c.dataDir);await S.clearRuntimeState();const _=new lt(b.join(c.dataDir,"proxy","relay-state.json"),{warn:e=>i.warn("main",e)}),N=new dt(b.join(c.dataDir,"secrets"));o.setRelaySecretResolver(e=>N.get(e));const L={hasDirectConfig:e=>o.hasDirectRelayConfig(e),clearDirectConfig:(e,n)=>o.setAgentProvider(e,void 0,{deferRestart:n?.deferRestart===!0})},K=()=>({pm:f(),agents:o,direct:L,store:_,secrets:N}),U=S.getRelayAgents();if(U.length>0)try{await S.startIfAnyRelayAgent(),i.info("main",`mitm proxy started for relay agents: ${U.join(", ")} (per-agent; others stay direct)`)}catch(e){i.error("main",`mitm proxy FAILED to start (relay agents: ${U.join(", ")}): ${e}. These agents will REFUSE to start (they must not silently fall back to your own account). Fix the proxy or turn relay off for them. Other agents are unaffected.`)}else i.info("main",'mitm proxy idle (no agent has Grix relay enabled; enable: PUT /api/proxy/agents/<name>/enabled {"enabled":true})');let H=null;if(f()){const e=async()=>{const r=f();r&&await me(r,o)&&i.info("main","no agent uses Grix relay anymore; mitm proxy stopped")};o.setRelayEnvSettledHandler(()=>{const r=f();r&&ge(r,L,_),e().catch(a=>{i.warn("main",`failed to stop idle mitm proxy: ${a}`)})});const n=()=>{const r=f(),a=r.getRelayAgents(),d=r.getDegradedRelayAgents();return{enabled:a.length>0,relayAgents:a,staleRelayAgents:o.getAgentsWithStaleRelayEnv(),degradedRelayAgents:d,relayState:_.list(),runtime:r.getRuntimeInfo(),config:r.getConfigSnapshot()}},p=new pt,A={status:()=>n(),setAgentRelay:async(r,a,d)=>{const g=f(),E={info:w=>i.info("main",w),warn:w=>i.warn("main",w)},R=w=>o.getAgentsStatus().find(v=>v.name===w),y=R(r);if(!a){p.cancel(r);const w=await p.runExclusive(r,async()=>{const v=await Q(K(),E,{agentName:r,target:"off"});return{...n(),restarted:v.restarted,busy:v.busy,...v.pending?{pending:v.pending}:{}}});return o.reportRelayStateLocalChange(r),w}if(!y)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});if(!Fe(y.clientType)&&pe(y.clientType).length===0)throw Object.assign(new Error(`client type "${y.clientType??""}" does not support Grix relay`),{code:"UNSUPPORTED_CLIENT_TYPE"});const O=await p.runExclusive(r,async()=>{const w=p.epoch(r),v=R(r);if(!v)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});const re=String(v.agentId??"").trim();if(!re)throw new fe(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const ne=p.waitCancelled(r,w);let C;try{C=await Promise.race([o.fetchRelayCredential(r,{model:d}),ne.promise.then(()=>{throw new he("relay enable cancelled by a later disable","CANCELLED")})])}finally{ne.dispose()}p.assertCurrent(r,w);const{restarted:xe,busy:Pe,pending:oe}=await ue(g,o,R,E,{localName:r,agentId:re,virtualKey:C.apiKey,anthropicBaseUrl:C.anthropicBaseUrl,openaiBaseUrl:C.openaiBaseUrl,model:d,...C.directRelay!==void 0?{directRelay:C.directRelay}:{}},(Ie,ke,_e)=>o.setAgentProvider(Ie,ke,{deferRestart:_e?.deferRestart===!0}),{store:_,secrets:N,direct:L});if(p.epoch(r)!==w)throw await Q(K(),E,{agentName:r,target:"off"}),new he("relay enable cancelled by a later disable","CANCELLED");return{...n(),restarted:xe,busy:Pe,...oe?{pending:oe}:{}}});return o.reportRelayStateLocalChange(r),O},setAgentRelayCredential:async(r,a)=>{const d=f(),g=o.getAgentsStatus(),{restarted:E,busy:R,pending:y}=await ue(d,o,P=>g.find(O=>O.name===P),{info:P=>i.info("main",P),warn:P=>i.warn("main",P)},{localName:r,agentId:a.agentId,virtualKey:a.virtualKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}},(P,O,w)=>o.setAgentProvider(P,O,{deferRestart:w?.deferRestart===!0}),{store:_,secrets:N,direct:L});return o.reportRelayStateLocalChange(r),{...n(),restarted:E,busy:R,...y?{pending:y}:{}}},disableAll:async()=>{const r=f(),a={info:g=>i.info("main",g),warn:g=>i.warn("main",g)},d=new Set([...r.getRelayAgents(),...o.getDirectRelayConfigAgentNames(),...Object.keys(_.list())]);for(const g of d)p.cancel(g);for(const g of d)await p.runExclusive(g,()=>Q(K(),a,{agentName:g,target:"off"})),o.reportRelayStateLocalChange(g);return await me(r,o),n()},setRoute:async(r,a)=>{const d=a,g=f();return g.setRoute({routeKey:r,targetBaseUrl:d.targetBaseUrl,...d.headers?{headers:d.headers}:{},...d.model?{model:d.model}:{},...d.modelMap?{modelMap:d.modelMap}:{},...d.passthrough?{passthrough:!0}:{},...d.relayOnly?{relayOnly:!0}:{},...d.codexResponsesToChat?{codexResponsesToChat:!0}:{}}),await g.persistConfig(),n()},deleteRoute:async r=>{const a=f();a.deleteRoute(r),await a.persistConfig()},setDefaultRoute:async r=>{const a=f();return a.setDefaultRouteKey(r),await a.persistConfig(),n()},setInterceptHosts:async r=>{const a=f();return a.setInterceptHosts(r),await a.persistConfig(),n()}};H=A,x.setProxyHandler(A),o.setRelayStateApplier({getLocalState:r=>{const a=f();if(a.getRelayAgents().includes(r)){const g=o.getAgentsStatus().find(y=>y.name===r),E=`grix-gateway-${String(g?.clientType??"").trim().toLowerCase()}`;return{enabled:!0,model:a.getConfigSnapshot().routes?.find(y=>y.routeKey===E)?.model?.trim()||void 0}}const d=o.getAgentProvider(r);return d?.api_key?.trim()?{enabled:!0,model:d.model?.trim()||void 0}:{enabled:!1}},applyEnable:async(r,a,d)=>{const g=o.getAgentsStatus().find(y=>y.name===r),E=String(g?.agentId??"").trim();if(!E)throw new fe(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const R=await A.setAgentRelayCredential(r,{agentId:E,virtualKey:a.apiKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:d??a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}});return{restarted:R.restarted===!0,busy:R.busy===!0,...R.pending?{pending:R.pending}:{}}},applyDisable:async r=>{const a=await A.setAgentRelay(r,!1);return{restarted:a.restarted===!0,busy:a.busy===!0,...a.pending?{pending:a.pending}:{}}}})}M.setStatusProvider(()=>o.getAgentsStatus()),M.setMetaProvider(()=>{const e=o.getAgentsStatus();return{ws:{connected:e.filter(n=>n.wsConnected===!0).length,total:e.length},upgrade:yt()}});const X=new nt(b.join(c.dataDir,"remote-admin.json"));if(o.setRemoteAdminGate(()=>X.isEnabled()),await o.start(Rt),U.length>0){const e=o.getAgentsStatus();for(const n of U){const p=e.find(r=>r.name===n);if(!p)continue;const A=pe(p.clientType);A.length!==0&&await S.setAgentRelayEnabled(n,!0,{relayHosts:A}).catch(r=>{i.warn("main",`failed to reconcile relay hosts for "${n}": ${r}`)})}}ge(S,L,_);const ee=parseInt(D["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);x.setAgentHandler({list:()=>o.getAgentsStatus(),add:e=>o.addAgent(e),remove:e=>o.removeAgent(e),restart:e=>o.restartAgent(e),reload:()=>o.reload()}),x.setUpgradeHandler({check:()=>o.checkUpgrade(),trigger:()=>o.triggerUpgrade()}),x.setProbeHandler({probeAll:e=>o.probeAll(e),probeOne:(e,n)=>o.probeOne(e,n)}),x.setRemoteAdminHandler({isEnabled:()=>X.isEnabled(),setEnabled:e=>{const n=X.setEnabled(e);return i.info("main",`remote admin ${n?"enabled":"disabled"} via admin API`),n}}),x.setInstallHandler({listInstallable:()=>o.listInstallable(),installAgent:e=>o.installAgent(e),getInstallProgress:e=>o.getInstallProgress(e)});const te={listAgents:()=>o.getAgentsStatus(),enableManaged:(e,n,p)=>{if(!H)throw new ye(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return H.setAgentRelayCredential(e,{agentId:n,virtualKey:p.virtualKey,anthropicBaseUrl:p.anthropicBaseUrl,openaiBaseUrl:p.openaiBaseUrl,model:p.model,...p.directRelay!==void 0?{directRelay:p.directRelay}:{}})},disableManaged:e=>{if(!H)throw new ye(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return H.setAgentRelay(e,!1)},enableHermesProfile:(e,n)=>mt({agentId:e,baseUrl:n.openaiBaseUrl??"",virtualKey:n.virtualKey,model:n.model??""}),disableHermesProfile:e=>gt(e)};x.setRelayCredentialHandler({listHermesProfiles:()=>ut(),enable:async(e,n)=>{const{target:p,result:A}=await ht(te,e,n);return{target:p,...A}},disable:async e=>{const{target:n,result:p}=await ft(te,e);return{target:n,...p}}});{const e=await se({label:"admin",port:ee,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:n=>x.start(n)});e&&await De(e)}const ve=b.join(c.dataDir,"admin-token"),be=b.join(c.dataDir,"admin-port");it(ve,Re),ae(be,String(ee),"utf-8"),await G(c.daemonStatusFile,{state:"running",pid:process.pid,instance_id:$,updated_at:Date.now()}),process.send&&process.send("ready"),i.info("main","grix-connector ready")}At().catch(async t=>{const s=`Fatal: ${t instanceof Error?t.stack??t.message:String(t)}`;try{process.stderr.write(`${s}
|
|
43
|
+
`)}catch{}ie(!1),i.error("main",s),z(t,"startup"),W("startup",t);const m=J(c.daemonLockFile);m?.pid===process.pid&&m.instance_id===$&&await G(c.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:m.instance_id,updated_at:Date.now(),reason:"startup_error"}).catch(()=>{}),await B(),F(c.daemonLockFile).catch(()=>{}),T(),process.exit(1)});const vt=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function Ae(t){return t instanceof Error&&"code"in t?vt.has(t.code):!1}
|
package/dist/launcher.mjs
CHANGED
|
@@ -15,15 +15,23 @@
|
|
|
15
15
|
// 崩溃时间戳落在 `<store>/crashes.json`,launcher 自身被重拉也不会把计数归零。
|
|
16
16
|
// 收到 SIGTERM/SIGINT/SIGHUP 时转发给子进程并随其退出(服务停止语义)。
|
|
17
17
|
//
|
|
18
|
+
// 同一个 store 只允许一个 launcher:启动时抢 store 级单例锁,抢不到就退出,
|
|
19
|
+
// 避免两个 launcher 各自拉起一个 daemon、把对方的 daemon 记成崩溃、再用共享的
|
|
20
|
+
// crashes.json 叠加计数,几秒内把健康版本"三振出局"回滚掉。
|
|
21
|
+
//
|
|
18
22
|
// 本文件由 daemon 在启动时按 LAUNCHER_TEMPLATE_VERSION 比对并原子重写,改动行为时必须 +1。
|
|
19
|
-
export const LAUNCHER_TEMPLATE_VERSION =
|
|
23
|
+
export const LAUNCHER_TEMPLATE_VERSION = 3;
|
|
20
24
|
|
|
21
25
|
import { spawn } from 'node:child_process';
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
22
27
|
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
28
|
+
import { connect, createServer } from 'node:net';
|
|
23
29
|
import { dirname, join } from 'node:path';
|
|
24
30
|
import { fileURLToPath } from 'node:url';
|
|
25
31
|
|
|
26
32
|
const RESTART_EXIT_CODE = 75;
|
|
33
|
+
const LOCK_RETRY_ATTEMPTS = 20;
|
|
34
|
+
const LOCK_RETRY_INTERVAL_MS = 250;
|
|
27
35
|
const RAPID_CRASH_WINDOW_MS = 5 * 60 * 1000;
|
|
28
36
|
const RAPID_CRASH_LIMIT = 3;
|
|
29
37
|
const MAX_BACKOFF_MS = 30_000;
|
|
@@ -35,6 +43,79 @@ function resolveStoreDir() {
|
|
|
35
43
|
return dirname(real);
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
/**
|
|
47
|
+
* store 级单例锁的端点。Windows 用命名管道(进程消失即随之消失,不留残留);
|
|
48
|
+
* POSIX 用 store 里的 unix socket 文件——端口派生会和机器上无关服务撞车,
|
|
49
|
+
* 一旦撞上 launcher 就永远起不来,socket 路径没有这个风险。
|
|
50
|
+
*/
|
|
51
|
+
function launcherLockEndpoint(store, platform = process.platform) {
|
|
52
|
+
if (platform === 'win32') {
|
|
53
|
+
const digest = createHash('sha256').update(store).digest('hex');
|
|
54
|
+
return `\\\\.\\pipe\\grix-launcher-${digest.slice(0, 24)}`;
|
|
55
|
+
}
|
|
56
|
+
return join(store, 'launcher.lock.sock');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listenOnce(server, endpoint) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const onError = (error) => {
|
|
62
|
+
if (error?.code === 'EADDRINUSE' || error?.code === 'EACCES') resolve(error.code);
|
|
63
|
+
else reject(error);
|
|
64
|
+
};
|
|
65
|
+
server.once('error', onError);
|
|
66
|
+
server.once('listening', () => {
|
|
67
|
+
server.removeListener('error', onError);
|
|
68
|
+
server.on('error', () => { /* 锁只需活到进程结束,后续错误无需处理 */ });
|
|
69
|
+
// 锁只用监听端点本身表态,探活连上来就断掉,别攒着不放。
|
|
70
|
+
server.on('connection', (socket) => socket.destroy());
|
|
71
|
+
resolve(null);
|
|
72
|
+
});
|
|
73
|
+
server.listen(endpoint);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** unix socket 文件在 launcher 被强杀后会留下,连得上才说明真有 launcher 在跑。 */
|
|
78
|
+
function isEndpointAlive(endpoint) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
let settled = false;
|
|
81
|
+
const socket = connect(endpoint);
|
|
82
|
+
const done = (alive) => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
try { socket.destroy(); } catch { /* already gone */ }
|
|
86
|
+
resolve(alive);
|
|
87
|
+
};
|
|
88
|
+
socket.once('connect', () => done(true));
|
|
89
|
+
socket.once('error', () => done(false));
|
|
90
|
+
setTimeout(() => done(false), 1000).unref?.();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 抢 store 级单例锁。拿到返回持锁的 server(进程退出时由系统释放),
|
|
96
|
+
* 已被别的 launcher 持有返回 null。
|
|
97
|
+
*
|
|
98
|
+
* 锁机制本身出错(既不是占用也不是权限)时向上抛:调用方按放行处理——
|
|
99
|
+
* launcher 起不来比偶尔多一个 launcher 严重得多。
|
|
100
|
+
*/
|
|
101
|
+
export async function acquireLauncherStoreLock(store, deps = {}) {
|
|
102
|
+
const endpoint = deps.lockEndpoint ?? launcherLockEndpoint(store, deps.platform);
|
|
103
|
+
const create = deps.createServer ?? createServer;
|
|
104
|
+
const isSocketPath = typeof endpoint === 'string' && !endpoint.startsWith('\\\\');
|
|
105
|
+
let server = create();
|
|
106
|
+
let blocked = await listenOnce(server, endpoint);
|
|
107
|
+
if (blocked && isSocketPath) {
|
|
108
|
+
if (await isEndpointAlive(endpoint)) return null;
|
|
109
|
+
// 空壳 socket:上一轮 launcher 被强杀留下的,删掉再抢一次。
|
|
110
|
+
try { rmSync(endpoint, { force: true }); } catch { return null; }
|
|
111
|
+
server = create();
|
|
112
|
+
blocked = await listenOnce(server, endpoint);
|
|
113
|
+
}
|
|
114
|
+
if (blocked) return null;
|
|
115
|
+
server.unref?.();
|
|
116
|
+
return server;
|
|
117
|
+
}
|
|
118
|
+
|
|
38
119
|
function readPointer(store, name) {
|
|
39
120
|
try {
|
|
40
121
|
const value = readFileSync(join(store, name), 'utf8').trim();
|
|
@@ -183,6 +264,27 @@ async function main() {
|
|
|
183
264
|
const store = resolveStoreDir();
|
|
184
265
|
const args = process.argv.slice(2);
|
|
185
266
|
mkdirSync(join(store, 'health'), { recursive: true });
|
|
267
|
+
|
|
268
|
+
// 升级交接时新旧 launcher 会短暂重叠,退几拍再判定,别把交接当成重复实例。
|
|
269
|
+
let storeLock = null;
|
|
270
|
+
let lockError = null;
|
|
271
|
+
for (let attempt = 0; attempt < LOCK_RETRY_ATTEMPTS && !storeLock; attempt++) {
|
|
272
|
+
try {
|
|
273
|
+
storeLock = await acquireLauncherStoreLock(store);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
lockError = error;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (!storeLock) await sleep(LOCK_RETRY_INTERVAL_MS);
|
|
279
|
+
}
|
|
280
|
+
if (lockError) {
|
|
281
|
+
log(store, `store lock unavailable (${lockError instanceof Error ? lockError.message : lockError}); continuing without it`);
|
|
282
|
+
} else if (!storeLock) {
|
|
283
|
+
// 退出码 0:supervisor 见 0 不会当成崩溃,也不会去动 crashes.json。
|
|
284
|
+
log(store, 'another launcher already holds this store; exiting without starting a daemon');
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
|
|
186
288
|
let backoffMs = 1000;
|
|
187
289
|
log(store, `started node=${process.version} store=${store} template=${LAUNCHER_TEMPLATE_VERSION}`);
|
|
188
290
|
|