@shogo-ai/worker 1.10.1 → 1.10.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/cli.mjs CHANGED
@@ -8,9 +8,9 @@ Starting with ${Y} optional host(s) blocked. See docs/my-machines-networking.md.
8
8
  Preflight failed — fix blocking issues before starting.
9
9
  `));return!X}import{accessSync as D$,constants as x$,existsSync as P$}from"node:fs";import{delimiter as S$,join as R$}from"node:path";function u($={}){let X=$.env??process.env,Y=_$($);for(let J of Y)if(z2(J.path))return J;let Z=$.systemBinName??Z2(),z=E$(Z,X.PATH);if(z)return{path:z,source:"path"};return null}function _$($){let X=$.env??process.env,Y=[];if($.flag&&$.flag.trim())Y.push({path:$.flag.trim(),source:"flag"});let Z=X.SHOGO_AGENT_RUNTIME_BIN?.trim();if(Z)Y.push({path:Z,source:"env"});return Y.push({path:R,source:"home"}),Y}function Z2(){return process.platform==="win32"?"shogo-agent-runtime.exe":"shogo-agent-runtime"}function z2($){if(!P$($))return!1;try{if(process.platform==="win32")return!0;return D$($,x$.X_OK),!0}catch{return!1}}function E$($,X){if(!X)return null;for(let Y of X.split(S$)){if(!Y)continue;let Z=R$(Y,$);if(z2(Z))return Z}return null}function d($={}){let X=$.env??process.env,Y=[];if(Y.push("Error: agent-runtime binary not found."),Y.push(""),Y.push("Looked in (priority order):"),$.flag)Y.push(` --runtime-bin ${$.flag}`);if(X.SHOGO_AGENT_RUNTIME_BIN)Y.push(` $SHOGO_AGENT_RUNTIME_BIN = ${X.SHOGO_AGENT_RUNTIME_BIN}`);return Y.push(` ${R} (default install location)`),Y.push(` ${Z2()} on $PATH`),Y.push(""),Y.push("Fix:"),Y.push(" - Run `shogo runtime install` to download the latest binary, or"),Y.push(" - Pass `--runtime-bin <path>` to point at an existing build."),Y.join(`
10
10
  `)}import{spawn as p$,spawnSync as c$}from"node:child_process";import{createHmac as C2,randomBytes as o$}from"node:crypto";import{existsSync as P0,mkdirSync as q2,readdirSync as a$}from"node:fs";import{createConnection as l$}from"node:net";import{tmpdir as r$}from"node:os";import{dirname as e$,join as X0}from"node:path";import{CloudFileTransport as S0}from"@shogo-ai/sdk/cloud-file-transport";import{watch as K2,statSync as I$}from"node:fs";import{relative as XX,sep as u$,posix as f$}from"node:path";var d$=1500,n$=new Set(["node_modules",".git","dist","build",".vite",".cache"]),i$=[".shogo/"];function g$($){if(!$)return!0;return $.split("/").some((X)=>n$.has(X))}class $0{rootDir;transport;debounceMs;logger;onFlush;mode;git;commitAndPush;watcher=null;pending=new Set;timer=null;flushing=!1;stopped=!1;constructor($){if(this.rootDir=$.rootDir,this.transport=$.transport,this.debounceMs=$.debounceMs??d$,this.logger=$.logger??console,this.onFlush=$.onFlush,this.mode=$.mode??"files",this.mode==="git"&&!$.git)throw Error('CloudSyncWatcher: mode: "git" requires the `git` option block');this.git=$.git,this.commitAndPush=$.commitAndPush??(async(X)=>{return(await Promise.resolve().then(() => (W0(),J2))).commitAndPush(X)})}start(){if(this.watcher)return;if(this.stopped)throw Error("CloudSyncWatcher: already stopped, build a fresh instance");try{this.watcher=K2(this.rootDir,{recursive:!0},($,X)=>{if(!X)return;this.handleEvent(String(X))})}catch($){this.logger.warn(`[CloudSyncWatcher] recursive watch failed (${$?.message??$}); falling back to root-only watch.`),this.watcher=K2(this.rootDir,(X,Y)=>{if(!Y)return;this.handleEvent(String(Y))})}}async stop(){if(this.stopped)return;if(this.stopped=!0,this.timer)clearTimeout(this.timer),this.timer=null;if(this.watcher){try{this.watcher.close()}catch{}this.watcher=null}if(this.pending.size>0)await this.flush()}handleEvent($){if(this.stopped)return;let X=$.split(u$).join(f$.sep);if(g$(X))return;try{let Y=`${this.rootDir}/${X}`;if(I$(Y).isDirectory())return}catch{return}this.pending.add(X),this.scheduleFlush()}scheduleFlush(){if(this.timer)return;this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.debounceMs)}async flush(){if(this.flushing){this.scheduleFlush();return}if(this.pending.size===0)return;this.flushing=!0;let $=Array.from(this.pending);this.pending.clear();try{if(this.mode==="git")await this.flushGit($);else await this.flushFiles($)}catch(X){this.logger.error(`[CloudSyncWatcher] flush failed: ${X?.message??X}`);for(let Y of $)this.pending.add(Y);this.scheduleFlush()}finally{this.flushing=!1}}async flushFiles($){if($.length===0)return;let X=await this.transport.uploadFiles($);if(this.onFlush?.({uploaded:$,errors:X.errors.length}),X.errors.length>0)for(let Y of X.errors)this.logger.warn(`[CloudSyncWatcher] upload ${Y.path}: ${Y.message}`)}async flushGit($){if(!this.git)throw Error("CloudSyncWatcher: missing git options");let X=[],Y=[];for(let J of $)if(i$.some((K)=>J===K.slice(0,-1)||J.startsWith(K)))X.push(J);else Y.push(J);if(X.length>0){let J=await this.transport.uploadFiles(X);if(J.errors.length>0)for(let K of J.errors)this.logger.warn(`[CloudSyncWatcher] upload ${K.path}: ${K.message}`)}let Z=!1,z;if(Y.length>0){let J=`auto: ${new Date().toISOString()}`;try{let K=await this.commitAndPush({apiUrl:this.git.apiUrl,apiKey:this.git.apiKey,projectId:this.git.projectId,localDir:this.rootDir,message:J,branch:this.git.branch,authorEmail:this.git.authorEmail,authorName:this.git.authorName,logger:this.logger});Z=K.committed,z=K.commitSha}catch(K){throw K}}this.onFlush?.({uploaded:$,errors:0,committed:Z,commitSha:z})}}W0();var R0=37100,_0=37900,V2=1,s$=2,k0=16,t$=900000,$6=30000,X6=1000,Y6=60000,Q2=8,H2=300000,Z6=60000,z6=500,J6=30000,w2=25000,K6=5000,q6=500,V6=($)=>({command:$,args:[]}),E0=null;function L2(){if(!E0)E0=o$(32).toString("hex");return E0}function W2($){return C2("sha256",L2()).update(`runtime:${$}`).digest("hex")}function Q6($){return C2("sha256",L2()).update(`webhook:${$}`).digest("hex")}function k2($){let X=$.indexOf("?");if(X===-1)return{pathname:$,search:""};return{pathname:$.slice(0,X),search:$.slice(X)}}function G2($){try{return a$($).length===0}catch{return!0}}function b0($,X,Y){let Z=[];switch(Z.push(`Cannot spawn agent-runtime for project ${$}: no workspace directory available.`),Z.push(""),X){case"no-auto-pull-config":Z.push(" Reason: WorkerRuntimeManager was constructed without an `autoPull` config and no caller-provided `projectDir` was found on disk."),Z.push(" This usually means a programmatic embedder forgot to wire up enrichSpawnConfig "+"or autoPull. CLI users should not see this — please file a bug.");break;case"no-projects-dir":Z.push(" Reason: auto-pull is configured but `projectsDir` is empty. The worker needs a persistent root directory under which it can store cloned project workspaces.");break;case"auto-pull-disabled":Z.push(` Reason: auto-pull was disabled (--no-auto-pull) and the expected pre-pulled workspace at ${Y} is missing or empty.`);break}return Z.push(""),Z.push(" How to fix (pick one):"),Z.push(" 1. Re-enable auto-pull (default). Drop the --no-auto-pull flag and restart"),Z.push(" the worker. The first inbound request for this project will clone its"),Z.push(" workspace from Shogo Cloud into <projectsDir>/<projectId>/."),Z.push(""),Z.push(" 2. Pre-pull manually with `shogo project pull <projectId>` before starting"),Z.push(" the worker. Use this when you want full control over when the clone runs"),Z.push(" (slow links, scheduled maintenance windows, etc.)."),Z.push(""),Z.push(" 3. Point the worker at an existing workspace by setting either:"),Z.push(" --projects-dir <path> (per-invocation flag)"),Z.push(" SHOGO_PROJECTS_DIR=<path> (env var, persists across restarts)"),Z.push(" shogo config set projectsDir <path>"),Z.push(" Whichever path you pick must contain a subdirectory named after the "),Z.push(` project id (e.g. <path>/${$}/) populated with the project's source.`),Z.push(""),Z.push(" Docs: https://shogo.ai/docs/self-hosted-worker#workspace-seeding"),Z.join(`
11
- `)}class v0{opts;log;runtimes=new Map;usedPorts=new Set;spawnCommand;resolved=null;stopped=!1;watchers=new Map;pulledProjects=new Set;syncModes=new Map;constructor($={}){this.opts=$,this.log=$.logger??console,this.spawnCommand=$.spawnCommand??V6}resolveBinary(){if(!this.resolved)this.resolved=this.opts.resolveBin?this.opts.resolveBin():u({flag:this.opts.runtimeBin,env:this.opts.env});return this.resolved}async resolveLocalUrl($,X){let{pathname:Y,search:Z}=k2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return null;if(!X){let K=this.getActiveProjects();if(K.length!==1)return null;X=K[0]}let z=await this.spawnConfigFor(X);if(!z)return this.log.warn(`[WorkerRuntimeManager] No spawn config for ${X} — set defaultSpawnConfig or enrichSpawnConfig`),null;let J=await this.ensureRunning(X,z);if(!J.agentPort)return null;return this.touch(X),`http://127.0.0.1:${J.agentPort}${Y}${Z}`}deriveRuntimeToken($){return W2($)}describeRejection($,X){let{pathname:Y}=k2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return{code:"CLI_WORKER_HAS_NO_DATA_API",message:`cli-worker only serves /agent/* paths; tried: ${Y}`};return{code:"CLI_WORKER_NO_PROJECT_FOR_PATH",message:`cli-worker received an /agent path without a single active project; projectId=${X??"none"}, path=${Y}`}}async spawnConfigFor($){let X=this.opts.defaultSpawnConfig;if(!X)return null;if(this.opts.enrichSpawnConfig)try{return await this.opts.enrichSpawnConfig($,X)}catch(Y){this.log.warn(`[WorkerRuntimeManager] enrichSpawnConfig failed for ${$}: ${Y?.message??Y}`)}return X}async ensureRunning($,X){if(this.stopped)throw Error("WorkerRuntimeManager is stopped");let Y=this.runtimes.get($);if(Y?.status==="failed")throw Error(`[WorkerRuntimeManager] cannot ensureRunning(${$}): ${Y.lastError??"runtime is in failed state"}. Call resetFailure(${$}) or stop(${$}) before retrying.`);X=await this.maybeAutoPull($,X);let Z=this.runtimes.get($);if(Z?.status==="running")return this.touch($),this.snapshot(Z);if(Z?.startPromise){let J=await Z.startPromise;return this.snapshot(J)}let z=Z??this.makeSlot($,X);if(!Z)this.runtimes.set($,z);z.spawnConfig=X,z.startPromise=this.doStart(z);try{let J=await z.startPromise;return this.enforceMaxRuntimes($),this.snapshot(J)}finally{z.startPromise=null}}enforceMaxRuntimes($){let X=this.opts.maxRuntimes;if(X==null||!Number.isFinite(X)||X<=0)return;let Y=Date.now(),Z=Array.from(this.runtimes.values()).filter((K)=>K.status==="running");if(Z.length<=X)return;let z=Z.filter((K)=>K.projectId!==$&&Y-K.lastUsedAt>=$6).sort((K,q)=>K.lastUsedAt-q.lastUsedAt),J=Z.length-X;for(let K of z){if(J<=0)break;let q=Y-K.lastUsedAt;this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} exceeded (${Z.length} running) — `+`LRU-evicting ${K.projectId} (idle ${Math.round(q/1000)}s)`),this.stop(K.projectId).catch((Q)=>{this.log.warn(`[WorkerRuntimeManager] maxRuntimes eviction of ${K.projectId} failed: ${Q?.message??Q}`)}),J--}if(J>0)this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} still exceeded by ${J} after eviction pass — `+"remaining over-cap slots are mid-stream; will retry on next spawn / idle reap")}async ensurePulled($,X){return this.maybeAutoPull($,X)}async maybeAutoPull($,X){let Y=this.opts.autoPull;if(X.projectDir&&P0(X.projectDir))return X;if(!Y)throw Error(b0($,"no-auto-pull-config",null));if(!Y.projectsDir)throw Error(b0($,"no-projects-dir",null));if(!Y.enabled){let K=X0(Y.projectsDir,$);if(P0(K)&&!G2(K))return{...X,projectDir:K};throw Error(b0($,"auto-pull-disabled",K))}if(this.pulledProjects.has($))return{...X,projectDir:X0(Y.projectsDir,$)};let Z=X0(Y.projectsDir,$),z=Y.logger??this.log,J={cloneProject:Y.gitOps?.cloneProject??x0,gitIsAvailable:Y.gitOps?.gitIsAvailable??D0,isGitRepo:Y.gitOps?.isGitRepo??t};this.pulledProjects.add($);try{q2(Z,{recursive:!0});let K=G2(Z),q=J.isGitRepo(Z),H=(Y.useGit!==!1?await J.gitIsAvailable():!1)&&(K||q)?"git":"files";if(this.syncModes.set($,H),H==="git"){if(K){z.log(`[WorkerRuntimeManager] auto-pull: git clone project ${$} into ${Z}`);try{let w=await J.cloneProject({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z,shallow:!0,logger:z});z.log(`[WorkerRuntimeManager] auto-pull: ${$} cloned at ${w.commitSha.slice(0,8)}`)}catch(w){z.warn(`[WorkerRuntimeManager] auto-pull: git clone failed for ${$} (${w?.message??w}); falling back to CloudFileTransport.downloadAll`),this.syncModes.set($,"files"),await this.fileTransportClone($,Z,X,z)}}else if(q)z.log(`[WorkerRuntimeManager] auto-pull: ${$} already has .git/; skipping clone`);if(this.syncModes.get($)==="git")await this.topUpShogoState($,Z,X,z)}else if(K)await this.fileTransportClone($,Z,X,z);else z.log(`[WorkerRuntimeManager] auto-pull: ${$} workspace already populated; skipping clone`);if(Y.watch!==!1&&!this.watchers.has($))try{let w=new S0({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z}),W=this.syncModes.get($)??"files",C=new $0({rootDir:Z,transport:w,logger:z,mode:W,git:W==="git"?{apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$}:void 0});C.start(),this.watchers.set($,C)}catch(w){z.warn(`[WorkerRuntimeManager] auto-pull: watcher start failed for ${$}: ${w?.message??w}`)}}catch(K){z.warn(`[WorkerRuntimeManager] auto-pull: failed for ${$} — runtime will fall back to template defaults. `+`(${K?.message??K})`)}return{...X,projectDir:Z}}async fileTransportClone($,X,Y,Z){Z.log(`[WorkerRuntimeManager] auto-pull: file-transport clone of ${$} into ${X}`);let J=await new S0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}).downloadAll();Z.log(`[WorkerRuntimeManager] auto-pull: ${$} downloaded ${J.downloaded} files (${J.errors.length} errors)`)}async topUpShogoState($,X,Y,Z){try{let z=new S0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}),K=(await z.listManifest()).filter((Q)=>Q.path===".shogo"||Q.path.startsWith(".shogo/"));if(K.length===0)return;let q=await z.downloadFiles(K);Z.log(`[WorkerRuntimeManager] auto-pull: ${$} .shogo/ top-up downloaded ${q.downloaded} files (${q.errors.length} errors)`)}catch(z){Z.warn(`[WorkerRuntimeManager] auto-pull: .shogo top-up failed for ${$}: ${z?.message??z}`)}}status($){let X=this.runtimes.get($);return X?this.snapshot(X):null}getActiveProjects(){return Array.from(this.runtimes.keys()).filter(($)=>{let X=this.runtimes.get($);return X&&(X.status==="running"||X.status==="starting"||X.status==="restarting")})}touch($){let X=this.runtimes.get($);if(!X)return;X.lastUsedAt=Date.now(),this.armIdleTimer(X)}async stop($,X="SIGTERM"){let Y=this.runtimes.get($);if(!Y)return;if(Y.status="stopping",Y.restartTimer)clearTimeout(Y.restartTimer),Y.restartTimer=null;if(Y.idleTimer)clearTimeout(Y.idleTimer),Y.idleTimer=null;if(Y.graceTimer)clearTimeout(Y.graceTimer),Y.graceTimer=null;if(Y.proc){this.killProcessGroup(Y,X);try{Y.proc.kill(X)}catch{}await this.waitForExit(Y.proc,5000),this.killProcessGroup(Y,"SIGKILL")}Y.pid=null,this.releasePort(Y.agentPort),this.runtimes.delete($)}resetFailure($){let X=this.runtimes.get($);if(!X||X.status!=="failed")return!1;if(X.restartTimer)clearTimeout(X.restartTimer),X.restartTimer=null;if(X.idleTimer)clearTimeout(X.idleTimer),X.idleTimer=null;if(X.graceTimer)clearTimeout(X.graceTimer),X.graceTimer=null;return this.runtimes.delete($),this.log.log(`[WorkerRuntimeManager] resetFailure: ${$} cleared, next ensureRunning will respawn`),!0}async stopAll($="SIGTERM"){this.stopped=!0;let X=Array.from(this.watchers.keys());await Promise.all(X.map(async(Z)=>{let z=this.watchers.get(Z);if(this.watchers.delete(Z),z)try{await z.stop()}catch(J){this.log.warn(`[WorkerRuntimeManager] watcher stop ${Z}: ${J?.message??J}`)}}));let Y=Array.from(this.runtimes.keys());await Promise.all(Y.map((Z)=>this.stop(Z,$).catch((z)=>{this.log.error(`[WorkerRuntimeManager] Failed to stop ${Z}: ${z?.message??z}`)})))}makeSlot($,X){return{projectId:$,agentPort:0,apiServerPort:0,status:"starting",proc:null,pid:null,startedAt:0,lastStdoutAt:0,lastUsedAt:Date.now(),restarts:0,consecutiveFailures:0,lastFailureAt:0,graceTimer:null,restartTimer:null,idleTimer:null,spawnConfig:X,startPromise:null}}async doStart($){let X=this.resolveBinary();if(!X)throw $.status="error",$.lastError="agent-runtime binary not found (run `shogo runtime install`)",Error($.lastError);if(!$.agentPort)$.agentPort=await this.allocatePort(),$.apiServerPort=$.agentPort+V2;let Y=this.buildEnv($,X.path),Z=this.resolveCwd($),{command:z,args:J}=this.spawnCommand(X.path);this.log.log(`[WorkerRuntimeManager] Spawning agent-runtime for ${$.projectId} via ${z} ${J.join(" ")} (port=${$.agentPort}, source=${X.source})`);let K=process.platform!=="win32",q=p$(z,J,{cwd:Z,env:Y,detached:K,stdio:["ignore","pipe","pipe"]});if(K)try{q.unref()}catch{}$.proc=q,$.pid=q.pid??null,$.status="starting",$.startedAt=Date.now(),$.lastStdoutAt=$.startedAt,q.on("error",(V)=>{$.lastError=V?.message??String(V),this.log.error(`[WorkerRuntimeManager] spawn error for ${$.projectId}: ${$.lastError}`)}),q.on("exit",(V,H)=>{this.handleExit($,V,H)});let Q=`[runtime:${$.projectId.slice(0,8)}]`;q.stdout?.on("data",(V)=>{$.lastStdoutAt=Date.now();for(let H of V.toString().trimEnd().split(`
11
+ `)}class v0{opts;log;runtimes=new Map;usedPorts=new Set;spawnCommand;resolved=null;stopped=!1;watchers=new Map;pulledProjects=new Set;syncModes=new Map;constructor($={}){this.opts=$,this.log=$.logger??console,this.spawnCommand=$.spawnCommand??V6}resolveBinary(){if(!this.resolved)this.resolved=this.opts.resolveBin?this.opts.resolveBin():u({flag:this.opts.runtimeBin,env:this.opts.env});return this.resolved}async resolveLocalUrl($,X){let{pathname:Y,search:Z}=k2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return null;if(!X){let K=this.getActiveProjects();if(K.length!==1)return null;X=K[0]}let z=await this.spawnConfigFor(X);if(!z)return this.log.warn(`[WorkerRuntimeManager] No spawn config for ${X} — set defaultSpawnConfig or enrichSpawnConfig`),null;let J=await this.ensureRunning(X,z);if(!J.agentPort)return null;return this.touch(X),`http://127.0.0.1:${J.agentPort}${Y}${Z}`}deriveRuntimeToken($){return W2($)}describeRejection($,X){let{pathname:Y}=k2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return{code:"CLI_WORKER_HAS_NO_DATA_API",message:`cli-worker only serves /agent/* paths; tried: ${Y}`};return{code:"CLI_WORKER_NO_PROJECT_FOR_PATH",message:`cli-worker received an /agent path without a single active project; projectId=${X??"none"}, path=${Y}`}}async spawnConfigFor($){let X=this.opts.defaultSpawnConfig;if(!X)return null;if(this.opts.enrichSpawnConfig)try{return await this.opts.enrichSpawnConfig($,X)}catch(Y){this.log.warn(`[WorkerRuntimeManager] enrichSpawnConfig failed for ${$}: ${Y?.message??Y}`)}return X}async ensureRunning($,X){if(this.stopped)throw Error("WorkerRuntimeManager is stopped");let Y=this.runtimes.get($);if(Y?.status==="failed")throw Error(`[WorkerRuntimeManager] cannot ensureRunning(${$}): ${Y.lastError??"runtime is in failed state"}. Call resetFailure(${$}) or stop(${$}) before retrying.`);X=await this.maybeAutoPull($,X);let Z=this.runtimes.get($);if(Z?.status==="running")return this.touch($),this.snapshot(Z);if(Z?.startPromise){let J=await Z.startPromise;return this.snapshot(J)}let z=Z??this.makeSlot($,X);if(!Z)this.runtimes.set($,z);z.spawnConfig=X,z.startPromise=this.doStart(z);try{let J=await z.startPromise;return this.enforceMaxRuntimes($),this.snapshot(J)}finally{z.startPromise=null}}enforceMaxRuntimes($){let X=this.opts.maxRuntimes;if(X==null||!Number.isFinite(X)||X<=0)return;let Y=Date.now(),Z=Array.from(this.runtimes.values()).filter((K)=>K.status==="running");if(Z.length<=X)return;let z=Z.filter((K)=>K.projectId!==$&&Y-K.lastUsedAt>=$6).sort((K,q)=>K.lastUsedAt-q.lastUsedAt),J=Z.length-X;for(let K of z){if(J<=0)break;let q=Y-K.lastUsedAt;this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} exceeded (${Z.length} running) — `+`LRU-evicting ${K.projectId} (idle ${Math.round(q/1000)}s)`),this.stop(K.projectId).catch((Q)=>{this.log.warn(`[WorkerRuntimeManager] maxRuntimes eviction of ${K.projectId} failed: ${Q?.message??Q}`)}),J--}if(J>0)this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} still exceeded by ${J} after eviction pass — `+"remaining over-cap slots are mid-stream; will retry on next spawn / idle reap")}async ensurePulled($,X){return this.maybeAutoPull($,X)}async maybeAutoPull($,X){let Y=this.opts.autoPull;if(X.projectDir&&P0(X.projectDir))return X;if(!Y)throw Error(b0($,"no-auto-pull-config",null));if(!Y.projectsDir)throw Error(b0($,"no-projects-dir",null));if(!Y.enabled){let K=X0(Y.projectsDir,$);if(P0(K)&&!G2(K))return{...X,projectDir:K};throw Error(b0($,"auto-pull-disabled",K))}if(this.pulledProjects.has($))return{...X,projectDir:X0(Y.projectsDir,$)};let Z=X0(Y.projectsDir,$),z=Y.logger??this.log,J={cloneProject:Y.gitOps?.cloneProject??x0,gitIsAvailable:Y.gitOps?.gitIsAvailable??D0,isGitRepo:Y.gitOps?.isGitRepo??t};this.pulledProjects.add($);try{q2(Z,{recursive:!0});let K=G2(Z),q=J.isGitRepo(Z),H=(Y.useGit!==!1?await J.gitIsAvailable():!1)&&(K||q)?"git":"files";if(this.syncModes.set($,H),H==="git"){if(K){z.log(`[WorkerRuntimeManager] auto-pull: git clone project ${$} into ${Z}`);try{let w=await J.cloneProject({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z,shallow:!0,logger:z});z.log(`[WorkerRuntimeManager] auto-pull: ${$} cloned at ${w.commitSha.slice(0,8)}`)}catch(w){z.warn(`[WorkerRuntimeManager] auto-pull: git clone failed for ${$} (${w?.message??w}); falling back to CloudFileTransport.downloadAll`),this.syncModes.set($,"files"),await this.fileTransportClone($,Z,X,z)}}else if(q)z.log(`[WorkerRuntimeManager] auto-pull: ${$} already has .git/; skipping clone`);if(this.syncModes.get($)==="git")await this.topUpShogoState($,Z,X,z)}else if(K)await this.fileTransportClone($,Z,X,z);else z.log(`[WorkerRuntimeManager] auto-pull: ${$} workspace already populated; skipping clone`);if(Y.watch!==!1&&!this.watchers.has($))try{let w=new S0({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z}),W=this.syncModes.get($)??"files",C=new $0({rootDir:Z,transport:w,logger:z,mode:W,git:W==="git"?{apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$}:void 0});C.start(),this.watchers.set($,C)}catch(w){z.warn(`[WorkerRuntimeManager] auto-pull: watcher start failed for ${$}: ${w?.message??w}`)}}catch(K){z.warn(`[WorkerRuntimeManager] auto-pull: failed for ${$} — runtime will fall back to template defaults. `+`(${K?.message??K})`)}return{...X,projectDir:Z}}async fileTransportClone($,X,Y,Z){Z.log(`[WorkerRuntimeManager] auto-pull: file-transport clone of ${$} into ${X}`);let J=await new S0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}).downloadAll();Z.log(`[WorkerRuntimeManager] auto-pull: ${$} downloaded ${J.downloaded} files (${J.errors.length} errors)`)}async topUpShogoState($,X,Y,Z){try{let z=new S0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}),K=(await z.listManifest()).filter((Q)=>Q.path===".shogo"||Q.path.startsWith(".shogo/"));if(K.length===0)return;let q=await z.downloadFiles(K);Z.log(`[WorkerRuntimeManager] auto-pull: ${$} .shogo/ top-up downloaded ${q.downloaded} files (${q.errors.length} errors)`)}catch(z){Z.warn(`[WorkerRuntimeManager] auto-pull: .shogo top-up failed for ${$}: ${z?.message??z}`)}}shouldDeferToCloudSync($){return!!this.opts.autoPull?.enabled&&this.pulledProjects.has($)}status($){let X=this.runtimes.get($);return X?this.snapshot(X):null}getActiveProjects(){return Array.from(this.runtimes.keys()).filter(($)=>{let X=this.runtimes.get($);return X&&(X.status==="running"||X.status==="starting"||X.status==="restarting")})}touch($){let X=this.runtimes.get($);if(!X)return;X.lastUsedAt=Date.now(),this.armIdleTimer(X)}async stop($,X="SIGTERM"){let Y=this.runtimes.get($);if(!Y)return;if(Y.status="stopping",Y.restartTimer)clearTimeout(Y.restartTimer),Y.restartTimer=null;if(Y.idleTimer)clearTimeout(Y.idleTimer),Y.idleTimer=null;if(Y.graceTimer)clearTimeout(Y.graceTimer),Y.graceTimer=null;if(Y.proc){this.killProcessGroup(Y,X);try{Y.proc.kill(X)}catch{}await this.waitForExit(Y.proc,5000),this.killProcessGroup(Y,"SIGKILL")}Y.pid=null,this.releasePort(Y.agentPort),this.runtimes.delete($)}resetFailure($){let X=this.runtimes.get($);if(!X||X.status!=="failed")return!1;if(X.restartTimer)clearTimeout(X.restartTimer),X.restartTimer=null;if(X.idleTimer)clearTimeout(X.idleTimer),X.idleTimer=null;if(X.graceTimer)clearTimeout(X.graceTimer),X.graceTimer=null;return this.runtimes.delete($),this.log.log(`[WorkerRuntimeManager] resetFailure: ${$} cleared, next ensureRunning will respawn`),!0}async stopAll($="SIGTERM"){this.stopped=!0;let X=Array.from(this.watchers.keys());await Promise.all(X.map(async(Z)=>{let z=this.watchers.get(Z);if(this.watchers.delete(Z),z)try{await z.stop()}catch(J){this.log.warn(`[WorkerRuntimeManager] watcher stop ${Z}: ${J?.message??J}`)}}));let Y=Array.from(this.runtimes.keys());await Promise.all(Y.map((Z)=>this.stop(Z,$).catch((z)=>{this.log.error(`[WorkerRuntimeManager] Failed to stop ${Z}: ${z?.message??z}`)})))}makeSlot($,X){return{projectId:$,agentPort:0,apiServerPort:0,status:"starting",proc:null,pid:null,startedAt:0,lastStdoutAt:0,lastUsedAt:Date.now(),restarts:0,consecutiveFailures:0,lastFailureAt:0,graceTimer:null,restartTimer:null,idleTimer:null,spawnConfig:X,startPromise:null}}async doStart($){let X=this.resolveBinary();if(!X)throw $.status="error",$.lastError="agent-runtime binary not found (run `shogo runtime install`)",Error($.lastError);if(!$.agentPort)$.agentPort=await this.allocatePort(),$.apiServerPort=$.agentPort+V2;let Y=this.buildEnv($,X.path),Z=this.resolveCwd($),{command:z,args:J}=this.spawnCommand(X.path);this.log.log(`[WorkerRuntimeManager] Spawning agent-runtime for ${$.projectId} via ${z} ${J.join(" ")} (port=${$.agentPort}, source=${X.source})`);let K=process.platform!=="win32",q=p$(z,J,{cwd:Z,env:Y,detached:K,stdio:["ignore","pipe","pipe"]});if(K)try{q.unref()}catch{}$.proc=q,$.pid=q.pid??null,$.status="starting",$.startedAt=Date.now(),$.lastStdoutAt=$.startedAt,q.on("error",(V)=>{$.lastError=V?.message??String(V),this.log.error(`[WorkerRuntimeManager] spawn error for ${$.projectId}: ${$.lastError}`)}),q.on("exit",(V,H)=>{this.handleExit($,V,H)});let Q=`[runtime:${$.projectId.slice(0,8)}]`;q.stdout?.on("data",(V)=>{$.lastStdoutAt=Date.now();for(let H of V.toString().trimEnd().split(`
12
12
  `))if(H)this.log.log(`${Q} ${H}`)}),q.stderr?.on("data",(V)=>{$.lastStdoutAt=Date.now();for(let H of V.toString().trimEnd().split(`
13
- `))if(H)this.log.error(`${Q} ${H}`)});try{return await this.waitForHealth($,J6),$.status="running",$.lastUsedAt=Date.now(),this.armIdleTimer($),this.armGraceTimer($),$}catch(V){$.status="error",$.lastError=V?.message??String(V),this.killProcessGroup($,"SIGTERM");try{q.kill("SIGTERM")}catch{}throw this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,V}}killProcessGroup($,X){if(!$.pid)return;if(process.platform==="win32"){try{c$("taskkill",["/F","/T","/PID",String($.pid)],{stdio:"ignore",windowsHide:!0})}catch{}return}try{process.kill(-$.pid,X)}catch{}}armGraceTimer($){if($.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;$.graceTimer=setTimeout(()=>{if($.graceTimer=null,$.consecutiveFailures>0)$.consecutiveFailures=0},Z6);try{$.graceTimer.unref?.()}catch{}}buildEnv($,X){let Y=$.spawnConfig,Z={...this.opts.env??process.env,PROJECT_ID:$.projectId,PORT:String($.agentPort),API_SERVER_PORT:String($.apiServerPort),SKILL_SERVER_PORT:String($.apiServerPort),WORKSPACE_API_PORT_BASE:String($.agentPort+s$),NODE_ENV:"production",SHOGO_CLOUD_URL:Y.cloudUrl,SHOGO_API_URL:Y.cloudUrl,SHOGO_API_KEY:Y.apiKey,RUNTIME_AUTH_SECRET:W2($.projectId),WEBHOOK_TOKEN:Q6($.projectId)};if(Y.projectDir)Z.PROJECT_DIR=Y.projectDir,Z.WORKSPACE_DIR=Y.projectDir;if(this.opts.autoPull?.enabled)Z.SHOGO_CLOUD_SYNC="1";if(Y.aiProxyUrl)Z.AI_PROXY_URL=Y.aiProxyUrl;if(Y.aiProxyToken)Z.AI_PROXY_TOKEN=Y.aiProxyToken;if(Y.techStackId)Z.TECH_STACK_ID=Y.techStackId;if(Y.name)Z.AGENT_NAME=Y.name;if(Y.workspaceId)Z.WORKSPACE_ID=Y.workspaceId;if(!Z.TREE_SITTER_WASM_DIR)Z.TREE_SITTER_WASM_DIR=X0(e$(X),"tree-sitter-wasm");if(Y.extraEnv)Object.assign(Z,Y.extraEnv);return Z}resolveCwd($){let X=$.spawnConfig;if(X.projectDir&&P0(X.projectDir))return X.projectDir;let Y=this.opts.runtimeWorkDir??X0(r$(),"shogo-runtime",$.projectId);return q2(Y,{recursive:!0}),Y}handleExit($,X,Y){let Z=Y===null&&X===0;if(this.log.log(`[WorkerRuntimeManager] runtime ${$.projectId} exited (code=${X}, signal=${Y})`),$.proc=null,$.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;if($.status==="stopping"||this.stopped){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0;return}if(Z){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,this.runtimes.delete($.projectId);return}this.killProcessGroup($,"SIGKILL"),$.pid=null;let z=Date.now(),J=z-$.lastFailureAt<=H2;if($.consecutiveFailures=J?$.consecutiveFailures+1:1,$.lastFailureAt=z,$.restarts+=1,$.lastError=`exited code=${X} signal=${Y}`,$.consecutiveFailures>=Q2){if($.status="failed",$.lastError=`Circuit breaker tripped: ${$.consecutiveFailures} consecutive non-clean exits within ${Math.round(H2/1000)}s (last: code=${X} signal=${Y}). Most recent on macOS is jetsam OOM (signal=SIGKILL with code=null); the previous incarnation's vite/tsserver/preview-manager children were reaped to prevent further RSS growth. Stop, fix the workspace, and call resetFailure(projectId) (or stop(projectId)) to allow another spawn attempt.`,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,$.restartTimer)clearTimeout($.restartTimer),$.restartTimer=null;if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;this.log.error(`[WorkerRuntimeManager] ${$.lastError}`);return}let K=this.restartBackoffMs($.restarts);if($.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${$.projectId} in ${Math.round(K/1000)}s (restart #${$.restarts}, consecutive failures ${$.consecutiveFailures}/${Q2})`),$.restartTimer)clearTimeout($.restartTimer);$.restartTimer=setTimeout(()=>{if($.restartTimer=null,$.status==="failed"||this.stopped)return;$.startPromise=this.doStart($).then((q)=>{return $.startPromise=null,q}).catch((q)=>{return $.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${$.projectId} failed: ${q?.message??q}`),$})},K);try{$.restartTimer.unref?.()}catch{}}restartBackoffMs($){let X=Math.min(X6*Math.pow(2,Math.max(0,$-1)),Y6),Y=X*0.2*Math.random();return X+Y}armIdleTimer($){if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;let X=this.opts.idleMs??t$;if(!Number.isFinite(X)||X<=0)return;$.idleTimer=setTimeout(()=>{let Y=Date.now()-$.lastUsedAt;if(Y<X){this.armIdleTimer($);return}this.log.log(`[WorkerRuntimeManager] idle-evicting ${$.projectId} after ${Math.round(Y/1000)}s`),this.stop($.projectId).catch((Z)=>{this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${Z?.message??Z}`)})},X)}async allocatePort(){let $=_0-R0,X=Math.min($,50);for(let Y=0;Y<X;Y++){let Z=R0+Math.floor(Math.random()*$);if(Z+k0-1>_0)continue;let z=!0;for(let q=0;q<k0;q++)if(this.usedPorts.has(Z+q)){z=!1;break}if(!z)continue;let J=await this.isPortListening(Z),K=await this.isPortListening(Z+V2);if(J||K)continue;for(let q=0;q<k0;q++)this.usedPorts.add(Z+q);return Z}throw Error(`Cannot allocate port in range ${R0}-${_0} after ${X} attempts`)}releasePort($){if(!$)return;for(let X=0;X<k0;X++)this.usedPorts.delete($+X)}async isPortListening($){let X=new AbortController,Y=setTimeout(()=>X.abort(),250);try{return await fetch(`http://127.0.0.1:${$}/`,{method:"HEAD",signal:X.signal}),clearTimeout(Y),!0}catch{return clearTimeout(Y),!1}}tcpProbe($){return new Promise((X)=>{let Y=!1,Z=(J)=>{if(Y)return;Y=!0;try{z.destroy()}catch{}X(J)},z=l$({host:"127.0.0.1",port:$});z.setTimeout(q6),z.once("connect",()=>Z(!0)),z.once("error",()=>Z(!1)),z.once("timeout",()=>Z(!1))})}async waitForHealth($,X){let{agentPort:Y,proc:Z}=$;if(!Z)throw Error(`waitForHealth: slot ${$.projectId} has no spawned process`);let z=Date.now(),J=z+X,K=null,q=!1,Q=0,V=0,H=0,w=0,W=z;while(Date.now()<J){if(w++,Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);V++;let C=new AbortController,O=setTimeout(()=>C.abort(),1500);try{let k=await fetch(`http://127.0.0.1:${Y}/health`,{method:"GET",signal:C.signal});if(clearTimeout(O),k.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${$.projectId} on port ${Y} (HTTP ${k.status} after ${Date.now()-z}ms, ${w} iter, ${V} http)`);return}K=`HTTP /health returned ${k.status}`}catch(k){clearTimeout(O);let M=k?.name??"Error",g=k?.code??k?.cause?.code;K=`HTTP /health failed: ${M}${g?`(${g})`:""}: ${k?.message??k}`}if(H++,q=await this.tcpProbe(Y),q){Q=Date.now();let k=Date.now()-$.lastStdoutAt;if(k>=w2)throw Error(`agent-runtime wedged on port ${Y}: TCP listening but stdout silent for ${k}ms (> ${w2}ms window); ${V} /health attempts, last error: ${K??"n/a"}`)}let A=Date.now();if(A-W>=K6){let k=A-z,M=A-$.lastStdoutAt,g=Q>0?A-Q:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${$.projectId} port ${Y} (${(k/1000).toFixed(1)}s elapsed, tcpListening=${q}${g!=null?`(${g}ms ago)`:""}, lastStdout=${M}ms ago, ${V} http, ${H} tcp, lastError=${K??"n/a"})`),W=A}await new Promise((k)=>setTimeout(k,z6))}if(Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);throw Error(`Timeout waiting for agent-runtime /health on port ${Y} after ${w} iter (httpAttempts=${V}, tcpAttempts=${H}, tcpListening=${q}, lastError=${K??"n/a"})`)}async waitForExit($,X){if($.exitCode!==null||$.signalCode!=null||$.killed)return;await new Promise((Y)=>{let Z=setTimeout(()=>{try{$.kill("SIGKILL")}catch{}Y()},X);$.once("exit",()=>{clearTimeout(Z),Y()})})}snapshot($){return{projectId:$.projectId,status:$.status,agentPort:$.agentPort||void 0,apiServerPort:$.apiServerPort||void 0,pid:$.proc?.pid,startedAt:$.startedAt||void 0,lastUsedAt:$.lastUsedAt,restarts:$.restarts,lastError:$.lastError}}}import{hostname as m0,platform as h0,arch as j0}from"node:os";class U2 extends Error{code="TUNNEL_WS_HEADERS_UNSUPPORTED";constructor(){super("Tunnel WebSocket auth requires a runtime with WebSocket header support (Bun or Node >= 21). Current runtime does not support WebSocket constructor headers.");this.name="TunnelWebSocketHeaderSupportError"}}var Y0=60,G0=300,I0=3,H6=I0,w6=1800000,W6=25000,O2=1000,B2=60000,A2=3;class u0{opts;log;pollTimer=null;ws=null;wsIdleTimer=null;heartbeatTimer=null;stopped=!1;currentPollInterval=Y0;wsReconnectAttempt=0;lastHeartbeatError=null;consecutiveAuthFailures=0;consecutiveAuthSuccesses=0;serverPublishedWsUrl=null;activeAbortControllers=new Map;constructor($){this.opts=$,this.log=$.logger??console}start(){if(!this.opts.apiKey){this.log.log("[WorkerTunnel] No API key set, skipping tunnel");return}this.stopped=!1,this.wsReconnectAttempt=0,this.currentPollInterval=Y0,this.log.log("[WorkerTunnel] Starting heartbeat polling to Shogo Cloud..."),this.heartbeatLoop()}stop(){if(this.stopped=!0,this.pollTimer)clearTimeout(this.pollTimer),this.pollTimer=null;this.cleanupWs(),this.log.log("[WorkerTunnel] Tunnel stopped")}isConnected(){if(this.ws!==null&&this.ws.readyState===WebSocket.OPEN)return!0;return!this.stopped&&!!this.opts.apiKey&&this.lastHeartbeatError===null&&this.pollTimer!==null}getCloudUrl(){return this.opts.cloudUrl.replace(/\/$/,"")}getWsBaseUrl(){let $=(this.opts.wsUrlOverride||process.env.SHOGO_TUNNEL_WS_URL||"").trim();if($)return $.replace(/\/$/,"");if(this.serverPublishedWsUrl)return this.serverPublishedWsUrl.replace(/\/$/,"");return this.getCloudUrl().replace(/^http/,"ws")}buildWsUrl(){return`${this.getWsBaseUrl()}/api/instances/ws`}supportsWebSocketConstructorHeaders($=globalThis){if(typeof $.Bun<"u"||typeof $.process?.versions?.bun==="string")return!0;let X=$.process?.versions?.node;if(X){if(parseInt(X.split(".")[0]??"0",10)>=21)return!0}return!1}createTunnelWebSocket($,X,Y=globalThis){if(!this.supportsWebSocketConstructorHeaders(Y))throw new U2;return new WebSocket($,X)}getReconnectDelayMs(){let $=Math.min(O2*Math.pow(2,this.wsReconnectAttempt),B2),X=$*0.2*Math.random();return $+X}async collectMetadata(){let $={hostname:m0(),os:h0(),arch:j0(),uptime:process.uptime(),protocolVersion:A2,tunnelStatus:this.ws?.readyState===WebSocket.OPEN?"connected":"polling",kind:this.opts.kind??"cli-worker"};try{let X=this.opts.resolver.getActiveProjects();$.activeProjects=X.length,$.projects=X.map((Y)=>{let Z=this.opts.resolver.status(Y);return{projectId:Y,status:Z?.status??"unknown",agentPort:Z?.agentPort}})}catch{$.activeProjects=0}return $}async sendHeartbeat(){let $=this.getCloudUrl(),X=await this.collectMetadata(),Y=m0(),Z=this.opts.name??Y,z=await fetch(`${$}/api/instances/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.opts.apiKey,"x-shogo-kind":this.opts.kind??"cli-worker"},body:JSON.stringify({hostname:Y,name:Z,os:h0(),arch:j0(),kind:this.opts.kind??"cli-worker",metadata:X})});if(!z.ok)throw Error(`Heartbeat failed: HTTP ${z.status}`);let J=await z.json();if(typeof J.wsUrl==="string"&&J.wsUrl.length>0){if(J.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${J.wsUrl}`);this.serverPublishedWsUrl=J.wsUrl}return J}scheduleNextPoll($){if(this.stopped)return;if(this.pollTimer)clearTimeout(this.pollTimer);let X=($??this.currentPollInterval)*1000;this.pollTimer=setTimeout(()=>void this.heartbeatLoop(),X)}async heartbeatLoop(){if(this.stopped)return;if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.scheduleNextPoll(this.currentPollInterval);return}try{let $=await this.sendHeartbeat(),X=$.nextPollIn||Y0;if(this.consecutiveAuthFailures>=I0){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<H6){this.currentPollInterval=G0,this.scheduleNextPoll();return}}if(this.currentPollInterval=X,this.lastHeartbeatError)this.log.log("[WorkerTunnel] Heartbeat recovered"),this.lastHeartbeatError=null;if(this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0,$.wsRequested&&!this.ws){this.log.log("[WorkerTunnel] Cloud requested WebSocket — connecting..."),this.connectWs();return}}catch($){let X=$?.message??String($);if(/HTTP 40[13]\b/.test(X))this.consecutiveAuthFailures++,this.consecutiveAuthSuccesses=0;else this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0;if(X!==this.lastHeartbeatError)this.log.error(`[WorkerTunnel] Heartbeat error: ${X}`),this.lastHeartbeatError=X;if(this.consecutiveAuthFailures>=I0){let Z=`tunnel saw ${this.consecutiveAuthFailures} consecutive auth failures from Shogo Cloud`;try{this.opts.onAuthRevoked?.(Z)}catch(z){this.log.warn(`[WorkerTunnel] onAuthRevoked threw: ${z?.message??z}`)}if(this.currentPollInterval!==G0)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${G0}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=G0}else this.currentPollInterval=Y0}this.scheduleNextPoll()}async handleRequest($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;this.resetWsIdleTimer();let X=new AbortController;this.activeAbortControllers.set($.requestId,X);try{let Y=await this.resolveLocalUrl($.path,$.projectId);if(!Y){let K=this.opts.resolver.describeRejection?this.opts.resolver.describeRejection($.path,$.projectId):{code:"NO_LOCAL_RUNTIME",message:`no local runtime available for path: ${$.path}`};this.sendFrame({type:"response",requestId:$.requestId,status:502,headers:{"content-type":"application/json"},body:JSON.stringify({code:K.code,message:K.message,path:$.path})});return}let Z={...$.headers??{}};if($.projectId&&($.path.startsWith("/agent/")||$.path==="/agent")){let K=this.opts.resolver.deriveRuntimeToken($.projectId);if(K)Z["x-runtime-token"]=K}let z={method:$.method,headers:Z,signal:X.signal};if($.body&&$.method!=="GET"&&$.method!=="HEAD")z.body=$.body;let J=await fetch(Y,z);if($.stream){let K=J.body?.getReader();if(!K){this.sendFrame({type:"stream-error",requestId:$.requestId,error:"No response body for stream"});return}let q=new TextDecoder;try{while(!0){let{done:Q,value:V}=await K.read();if(Q)break;if(this.ws?.readyState!==WebSocket.OPEN)break;this.sendFrame({type:"stream-chunk",requestId:$.requestId,data:q.decode(V,{stream:!0})})}if(this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-end",requestId:$.requestId})}catch(Q){if(Q?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:$.requestId,error:Q?.message??String(Q)})}}else{let K=await J.text(),q={};J.headers.forEach((Q,V)=>{q[V]=Q}),this.sendFrame({type:"response",requestId:$.requestId,status:J.status,headers:q,body:K})}}catch(Y){if(Y?.name==="AbortError")return;if(this.ws?.readyState!==WebSocket.OPEN)return;let Z=$.stream?{type:"stream-error",requestId:$.requestId,error:Y?.message??String(Y)}:{type:"response",requestId:$.requestId,status:502,body:JSON.stringify({error:Y?.message??String(Y)})};this.sendFrame(Z)}finally{this.activeAbortControllers.delete($.requestId)}}async resolveLocalUrl($,X){return this.opts.resolver.resolveLocalUrl($,X)}sendFrame($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{this.ws.send(JSON.stringify($))}catch(X){this.log.warn(`[WorkerTunnel] Frame send failed: ${X?.message??X}`)}}resetWsIdleTimer(){if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer);this.wsIdleTimer=setTimeout(()=>{if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.log.log("[WorkerTunnel] WebSocket idle timeout — closing, returning to polling");try{this.ws.close(1000,"Idle timeout")}catch{}}},w6)}startWsHeartbeat(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.heartbeatTimer=setInterval(async()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{let $=await this.collectMetadata();this.sendFrame({type:"heartbeat",metadata:$})}catch{}},W6)}connectWs(){if(this.stopped||this.ws)return;let $=this.buildWsUrl(),X=m0(),Y=h0(),Z=j0(),z=this.opts.name??X;this.log.log(`[WorkerTunnel] Opening WebSocket to ${$} (hostname=${X})`);let J={headers:{Authorization:`Bearer ${this.opts.apiKey}`,"x-shogo-hostname":X,"x-shogo-name":z,"x-shogo-os":Y,"x-shogo-arch":Z,"x-shogo-kind":this.opts.kind??"cli-worker"}},K;try{K=this.createTunnelWebSocket($,J)}catch(q){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${q?.message??q}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=K,K.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},K.onmessage=(q)=>{let Q;try{let V=typeof q.data==="string"?q.data:q.data.toString();Q=JSON.parse(V)}catch{return}if(Q.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(Q.type==="cancel"){let V=this.activeAbortControllers.get(Q.requestId);if(V)V.abort();return}if(Q.type==="request"){this.handleRequest(Q).catch((V)=>{this.log.error(`[WorkerTunnel] Error handling request: ${V?.message??V}`)});return}},K.onclose=(q)=>{if(this.log.log(`[WorkerTunnel] WebSocket closed: code=${q.code} reason=${q.reason||"none"}`),this.cleanupWs(),this.stopped)return;if(q.code===1000||q.code===4000)this.scheduleNextPoll(this.currentPollInterval);else{this.wsReconnectAttempt++;let Q=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(Q/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(Q/1000))}},K.onerror=(q)=>{this.log.error("[WorkerTunnel] WebSocket error:",q?.message??"unknown")}}cleanupWs(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer),this.wsIdleTimer=null;for(let[,$]of this.activeAbortControllers)try{$.abort()}catch{}this.activeAbortControllers.clear(),this.ws=null}_testing(){let $=this;return{sendHeartbeat:()=>$.sendHeartbeat(),heartbeatLoop:()=>$.heartbeatLoop(),connectWs:()=>$.connectWs(),cleanupWs:()=>$.cleanupWs(),handleRequest:(X)=>$.handleRequest(X),installFakeWs:(X)=>{$.ws=X},getCloudUrl:()=>$.getCloudUrl(),getWsBaseUrl:()=>$.getWsBaseUrl(),buildWsUrl:()=>$.buildWsUrl(),getReconnectDelayMs:()=>$.getReconnectDelayMs(),supportsWebSocketConstructorHeaders:(X)=>$.supportsWebSocketConstructorHeaders(X),createTunnelWebSocket:(X,Y,Z)=>$.createTunnelWebSocket(X,Y,Z),DEFAULT_POLL_INTERVAL_S:Y0,BACKOFF_BASE_MS:O2,BACKOFF_MAX_MS:B2,TUNNEL_PROTOCOL_VERSION:A2,get currentPollInterval(){return $.currentPollInterval},set currentPollInterval(X){$.currentPollInterval=X},get wsReconnectAttempt(){return $.wsReconnectAttempt},set wsReconnectAttempt(X){$.wsReconnectAttempt=X},get ws(){return $.ws},get stopped(){return $.stopped},get serverPublishedWsUrl(){return $.serverPublishedWsUrl},set serverPublishedWsUrl(X){$.serverPublishedWsUrl=X}}}}async function F2($){let X=_({name:$.name,workerDir:$.workerDir,apiKey:$.apiKey,cloudUrl:$.cloudUrl,port:$.port?parseInt($.port,10):void 0,projectsDir:$.projectsDir}),Y=s0($.proxy);if(!$.foreground)return G6({cfg:X,proxy:Y,flags:$});let Z=u({flag:$.runtimeBin});if(!Z)console.error(G.red(d({flag:$.runtimeBin}))),process.exit(1);if(M0(process.env,Y),$.debug){if(!await Y2(X2({cloudUrl:X.cloudUrl,apiKey:X.apiKey,workerDir:X.workerDir,proxy:Y})))process.exit(1)}let z=!$.noAutoPull,J=!$.noGit;if(console.log(G.bold(`
13
+ `))if(H)this.log.error(`${Q} ${H}`)});try{return await this.waitForHealth($,J6),$.status="running",$.lastUsedAt=Date.now(),this.armIdleTimer($),this.armGraceTimer($),$}catch(V){$.status="error",$.lastError=V?.message??String(V),this.killProcessGroup($,"SIGTERM");try{q.kill("SIGTERM")}catch{}throw this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,V}}killProcessGroup($,X){if(!$.pid)return;if(process.platform==="win32"){try{c$("taskkill",["/F","/T","/PID",String($.pid)],{stdio:"ignore",windowsHide:!0})}catch{}return}try{process.kill(-$.pid,X)}catch{}}armGraceTimer($){if($.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;$.graceTimer=setTimeout(()=>{if($.graceTimer=null,$.consecutiveFailures>0)$.consecutiveFailures=0},Z6);try{$.graceTimer.unref?.()}catch{}}buildEnv($,X){let Y=$.spawnConfig,Z={...this.opts.env??process.env,PROJECT_ID:$.projectId,PORT:String($.agentPort),API_SERVER_PORT:String($.apiServerPort),SKILL_SERVER_PORT:String($.apiServerPort),WORKSPACE_API_PORT_BASE:String($.agentPort+s$),NODE_ENV:"production",SHOGO_CLOUD_URL:Y.cloudUrl,SHOGO_API_URL:Y.cloudUrl,SHOGO_API_KEY:Y.apiKey,RUNTIME_AUTH_SECRET:W2($.projectId),WEBHOOK_TOKEN:Q6($.projectId)};if(Y.projectDir)Z.PROJECT_DIR=Y.projectDir,Z.WORKSPACE_DIR=Y.projectDir;if(this.shouldDeferToCloudSync($.projectId))Z.SHOGO_CLOUD_SYNC="1";if(Y.aiProxyUrl)Z.AI_PROXY_URL=Y.aiProxyUrl;if(Y.aiProxyToken)Z.AI_PROXY_TOKEN=Y.aiProxyToken;if(Y.techStackId)Z.TECH_STACK_ID=Y.techStackId;if(Y.name)Z.AGENT_NAME=Y.name;if(Y.workspaceId)Z.WORKSPACE_ID=Y.workspaceId;if(!Z.TREE_SITTER_WASM_DIR)Z.TREE_SITTER_WASM_DIR=X0(e$(X),"tree-sitter-wasm");if(Y.extraEnv)Object.assign(Z,Y.extraEnv);return Z}resolveCwd($){let X=$.spawnConfig;if(X.projectDir&&P0(X.projectDir))return X.projectDir;let Y=this.opts.runtimeWorkDir??X0(r$(),"shogo-runtime",$.projectId);return q2(Y,{recursive:!0}),Y}handleExit($,X,Y){let Z=Y===null&&X===0;if(this.log.log(`[WorkerRuntimeManager] runtime ${$.projectId} exited (code=${X}, signal=${Y})`),$.proc=null,$.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;if($.status==="stopping"||this.stopped){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0;return}if(Z){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,this.runtimes.delete($.projectId);return}this.killProcessGroup($,"SIGKILL"),$.pid=null;let z=Date.now(),J=z-$.lastFailureAt<=H2;if($.consecutiveFailures=J?$.consecutiveFailures+1:1,$.lastFailureAt=z,$.restarts+=1,$.lastError=`exited code=${X} signal=${Y}`,$.consecutiveFailures>=Q2){if($.status="failed",$.lastError=`Circuit breaker tripped: ${$.consecutiveFailures} consecutive non-clean exits within ${Math.round(H2/1000)}s (last: code=${X} signal=${Y}). Most recent on macOS is jetsam OOM (signal=SIGKILL with code=null); the previous incarnation's vite/tsserver/preview-manager children were reaped to prevent further RSS growth. Stop, fix the workspace, and call resetFailure(projectId) (or stop(projectId)) to allow another spawn attempt.`,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,$.restartTimer)clearTimeout($.restartTimer),$.restartTimer=null;if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;this.log.error(`[WorkerRuntimeManager] ${$.lastError}`);return}let K=this.restartBackoffMs($.restarts);if($.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${$.projectId} in ${Math.round(K/1000)}s (restart #${$.restarts}, consecutive failures ${$.consecutiveFailures}/${Q2})`),$.restartTimer)clearTimeout($.restartTimer);$.restartTimer=setTimeout(()=>{if($.restartTimer=null,$.status==="failed"||this.stopped)return;$.startPromise=this.doStart($).then((q)=>{return $.startPromise=null,q}).catch((q)=>{return $.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${$.projectId} failed: ${q?.message??q}`),$})},K);try{$.restartTimer.unref?.()}catch{}}restartBackoffMs($){let X=Math.min(X6*Math.pow(2,Math.max(0,$-1)),Y6),Y=X*0.2*Math.random();return X+Y}armIdleTimer($){if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;let X=this.opts.idleMs??t$;if(!Number.isFinite(X)||X<=0)return;$.idleTimer=setTimeout(()=>{let Y=Date.now()-$.lastUsedAt;if(Y<X){this.armIdleTimer($);return}this.log.log(`[WorkerRuntimeManager] idle-evicting ${$.projectId} after ${Math.round(Y/1000)}s`),this.stop($.projectId).catch((Z)=>{this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${Z?.message??Z}`)})},X)}async allocatePort(){let $=_0-R0,X=Math.min($,50);for(let Y=0;Y<X;Y++){let Z=R0+Math.floor(Math.random()*$);if(Z+k0-1>_0)continue;let z=!0;for(let q=0;q<k0;q++)if(this.usedPorts.has(Z+q)){z=!1;break}if(!z)continue;let J=await this.isPortListening(Z),K=await this.isPortListening(Z+V2);if(J||K)continue;for(let q=0;q<k0;q++)this.usedPorts.add(Z+q);return Z}throw Error(`Cannot allocate port in range ${R0}-${_0} after ${X} attempts`)}releasePort($){if(!$)return;for(let X=0;X<k0;X++)this.usedPorts.delete($+X)}async isPortListening($){let X=new AbortController,Y=setTimeout(()=>X.abort(),250);try{return await fetch(`http://127.0.0.1:${$}/`,{method:"HEAD",signal:X.signal}),clearTimeout(Y),!0}catch{return clearTimeout(Y),!1}}tcpProbe($){return new Promise((X)=>{let Y=!1,Z=(J)=>{if(Y)return;Y=!0;try{z.destroy()}catch{}X(J)},z=l$({host:"127.0.0.1",port:$});z.setTimeout(q6),z.once("connect",()=>Z(!0)),z.once("error",()=>Z(!1)),z.once("timeout",()=>Z(!1))})}async waitForHealth($,X){let{agentPort:Y,proc:Z}=$;if(!Z)throw Error(`waitForHealth: slot ${$.projectId} has no spawned process`);let z=Date.now(),J=z+X,K=null,q=!1,Q=0,V=0,H=0,w=0,W=z;while(Date.now()<J){if(w++,Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);V++;let C=new AbortController,O=setTimeout(()=>C.abort(),1500);try{let k=await fetch(`http://127.0.0.1:${Y}/health`,{method:"GET",signal:C.signal});if(clearTimeout(O),k.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${$.projectId} on port ${Y} (HTTP ${k.status} after ${Date.now()-z}ms, ${w} iter, ${V} http)`);return}K=`HTTP /health returned ${k.status}`}catch(k){clearTimeout(O);let M=k?.name??"Error",g=k?.code??k?.cause?.code;K=`HTTP /health failed: ${M}${g?`(${g})`:""}: ${k?.message??k}`}if(H++,q=await this.tcpProbe(Y),q){Q=Date.now();let k=Date.now()-$.lastStdoutAt;if(k>=w2)throw Error(`agent-runtime wedged on port ${Y}: TCP listening but stdout silent for ${k}ms (> ${w2}ms window); ${V} /health attempts, last error: ${K??"n/a"}`)}let A=Date.now();if(A-W>=K6){let k=A-z,M=A-$.lastStdoutAt,g=Q>0?A-Q:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${$.projectId} port ${Y} (${(k/1000).toFixed(1)}s elapsed, tcpListening=${q}${g!=null?`(${g}ms ago)`:""}, lastStdout=${M}ms ago, ${V} http, ${H} tcp, lastError=${K??"n/a"})`),W=A}await new Promise((k)=>setTimeout(k,z6))}if(Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);throw Error(`Timeout waiting for agent-runtime /health on port ${Y} after ${w} iter (httpAttempts=${V}, tcpAttempts=${H}, tcpListening=${q}, lastError=${K??"n/a"})`)}async waitForExit($,X){if($.exitCode!==null||$.signalCode!=null||$.killed)return;await new Promise((Y)=>{let Z=setTimeout(()=>{try{$.kill("SIGKILL")}catch{}Y()},X);$.once("exit",()=>{clearTimeout(Z),Y()})})}snapshot($){return{projectId:$.projectId,status:$.status,agentPort:$.agentPort||void 0,apiServerPort:$.apiServerPort||void 0,pid:$.proc?.pid,startedAt:$.startedAt||void 0,lastUsedAt:$.lastUsedAt,restarts:$.restarts,lastError:$.lastError}}}import{hostname as m0,platform as h0,arch as j0}from"node:os";class U2 extends Error{code="TUNNEL_WS_HEADERS_UNSUPPORTED";constructor(){super("Tunnel WebSocket auth requires a runtime with WebSocket header support (Bun or Node >= 21). Current runtime does not support WebSocket constructor headers.");this.name="TunnelWebSocketHeaderSupportError"}}var Y0=60,G0=300,I0=3,H6=I0,w6=1800000,W6=25000,O2=1000,B2=60000,A2=3;class u0{opts;log;pollTimer=null;ws=null;wsIdleTimer=null;heartbeatTimer=null;stopped=!1;currentPollInterval=Y0;wsReconnectAttempt=0;lastHeartbeatError=null;consecutiveAuthFailures=0;consecutiveAuthSuccesses=0;serverPublishedWsUrl=null;activeAbortControllers=new Map;constructor($){this.opts=$,this.log=$.logger??console}start(){if(!this.opts.apiKey){this.log.log("[WorkerTunnel] No API key set, skipping tunnel");return}this.stopped=!1,this.wsReconnectAttempt=0,this.currentPollInterval=Y0,this.log.log("[WorkerTunnel] Starting heartbeat polling to Shogo Cloud..."),this.heartbeatLoop()}stop(){if(this.stopped=!0,this.pollTimer)clearTimeout(this.pollTimer),this.pollTimer=null;this.cleanupWs(),this.log.log("[WorkerTunnel] Tunnel stopped")}isConnected(){if(this.ws!==null&&this.ws.readyState===WebSocket.OPEN)return!0;return!this.stopped&&!!this.opts.apiKey&&this.lastHeartbeatError===null&&this.pollTimer!==null}getCloudUrl(){return this.opts.cloudUrl.replace(/\/$/,"")}getWsBaseUrl(){let $=(this.opts.wsUrlOverride||process.env.SHOGO_TUNNEL_WS_URL||"").trim();if($)return $.replace(/\/$/,"");if(this.serverPublishedWsUrl)return this.serverPublishedWsUrl.replace(/\/$/,"");return this.getCloudUrl().replace(/^http/,"ws")}buildWsUrl(){return`${this.getWsBaseUrl()}/api/instances/ws`}supportsWebSocketConstructorHeaders($=globalThis){if(typeof $.Bun<"u"||typeof $.process?.versions?.bun==="string")return!0;let X=$.process?.versions?.node;if(X){if(parseInt(X.split(".")[0]??"0",10)>=21)return!0}return!1}createTunnelWebSocket($,X,Y=globalThis){if(!this.supportsWebSocketConstructorHeaders(Y))throw new U2;return new WebSocket($,X)}getReconnectDelayMs(){let $=Math.min(O2*Math.pow(2,this.wsReconnectAttempt),B2),X=$*0.2*Math.random();return $+X}async collectMetadata(){let $={hostname:m0(),os:h0(),arch:j0(),uptime:process.uptime(),protocolVersion:A2,tunnelStatus:this.ws?.readyState===WebSocket.OPEN?"connected":"polling",kind:this.opts.kind??"cli-worker"};try{let X=this.opts.resolver.getActiveProjects();$.activeProjects=X.length,$.projects=X.map((Y)=>{let Z=this.opts.resolver.status(Y);return{projectId:Y,status:Z?.status??"unknown",agentPort:Z?.agentPort}})}catch{$.activeProjects=0}return $}async sendHeartbeat(){let $=this.getCloudUrl(),X=await this.collectMetadata(),Y=m0(),Z=this.opts.name??Y,z=await fetch(`${$}/api/instances/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.opts.apiKey,"x-shogo-kind":this.opts.kind??"cli-worker"},body:JSON.stringify({hostname:Y,name:Z,os:h0(),arch:j0(),kind:this.opts.kind??"cli-worker",metadata:X})});if(!z.ok)throw Error(`Heartbeat failed: HTTP ${z.status}`);let J=await z.json();if(typeof J.wsUrl==="string"&&J.wsUrl.length>0){if(J.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${J.wsUrl}`);this.serverPublishedWsUrl=J.wsUrl}return J}scheduleNextPoll($){if(this.stopped)return;if(this.pollTimer)clearTimeout(this.pollTimer);let X=($??this.currentPollInterval)*1000;this.pollTimer=setTimeout(()=>void this.heartbeatLoop(),X)}async heartbeatLoop(){if(this.stopped)return;if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.scheduleNextPoll(this.currentPollInterval);return}try{let $=await this.sendHeartbeat(),X=$.nextPollIn||Y0;if(this.consecutiveAuthFailures>=I0){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<H6){this.currentPollInterval=G0,this.scheduleNextPoll();return}}if(this.currentPollInterval=X,this.lastHeartbeatError)this.log.log("[WorkerTunnel] Heartbeat recovered"),this.lastHeartbeatError=null;if(this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0,$.wsRequested&&!this.ws){this.log.log("[WorkerTunnel] Cloud requested WebSocket — connecting..."),this.connectWs();return}}catch($){let X=$?.message??String($);if(/HTTP 40[13]\b/.test(X))this.consecutiveAuthFailures++,this.consecutiveAuthSuccesses=0;else this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0;if(X!==this.lastHeartbeatError)this.log.error(`[WorkerTunnel] Heartbeat error: ${X}`),this.lastHeartbeatError=X;if(this.consecutiveAuthFailures>=I0){let Z=`tunnel saw ${this.consecutiveAuthFailures} consecutive auth failures from Shogo Cloud`;try{this.opts.onAuthRevoked?.(Z)}catch(z){this.log.warn(`[WorkerTunnel] onAuthRevoked threw: ${z?.message??z}`)}if(this.currentPollInterval!==G0)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${G0}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=G0}else this.currentPollInterval=Y0}this.scheduleNextPoll()}async handleRequest($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;this.resetWsIdleTimer();let X=new AbortController;this.activeAbortControllers.set($.requestId,X);try{let Y=await this.resolveLocalUrl($.path,$.projectId);if(!Y){let K=this.opts.resolver.describeRejection?this.opts.resolver.describeRejection($.path,$.projectId):{code:"NO_LOCAL_RUNTIME",message:`no local runtime available for path: ${$.path}`};this.sendFrame({type:"response",requestId:$.requestId,status:502,headers:{"content-type":"application/json"},body:JSON.stringify({code:K.code,message:K.message,path:$.path})});return}let Z={...$.headers??{}};if($.projectId&&($.path.startsWith("/agent/")||$.path==="/agent")){let K=this.opts.resolver.deriveRuntimeToken($.projectId);if(K)Z["x-runtime-token"]=K}let z={method:$.method,headers:Z,signal:X.signal};if($.body&&$.method!=="GET"&&$.method!=="HEAD")z.body=$.body;let J=await fetch(Y,z);if($.stream){let K=J.body?.getReader();if(!K){this.sendFrame({type:"stream-error",requestId:$.requestId,error:"No response body for stream"});return}let q=new TextDecoder;try{while(!0){let{done:Q,value:V}=await K.read();if(Q)break;if(this.ws?.readyState!==WebSocket.OPEN)break;this.sendFrame({type:"stream-chunk",requestId:$.requestId,data:q.decode(V,{stream:!0})})}if(this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-end",requestId:$.requestId})}catch(Q){if(Q?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:$.requestId,error:Q?.message??String(Q)})}}else{let K=await J.text(),q={};J.headers.forEach((Q,V)=>{q[V]=Q}),this.sendFrame({type:"response",requestId:$.requestId,status:J.status,headers:q,body:K})}}catch(Y){if(Y?.name==="AbortError")return;if(this.ws?.readyState!==WebSocket.OPEN)return;let Z=$.stream?{type:"stream-error",requestId:$.requestId,error:Y?.message??String(Y)}:{type:"response",requestId:$.requestId,status:502,body:JSON.stringify({error:Y?.message??String(Y)})};this.sendFrame(Z)}finally{this.activeAbortControllers.delete($.requestId)}}async resolveLocalUrl($,X){return this.opts.resolver.resolveLocalUrl($,X)}sendFrame($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{this.ws.send(JSON.stringify($))}catch(X){this.log.warn(`[WorkerTunnel] Frame send failed: ${X?.message??X}`)}}resetWsIdleTimer(){if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer);this.wsIdleTimer=setTimeout(()=>{if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.log.log("[WorkerTunnel] WebSocket idle timeout — closing, returning to polling");try{this.ws.close(1000,"Idle timeout")}catch{}}},w6)}startWsHeartbeat(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.heartbeatTimer=setInterval(async()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{let $=await this.collectMetadata();this.sendFrame({type:"heartbeat",metadata:$})}catch{}},W6)}connectWs(){if(this.stopped||this.ws)return;let $=this.buildWsUrl(),X=m0(),Y=h0(),Z=j0(),z=this.opts.name??X;this.log.log(`[WorkerTunnel] Opening WebSocket to ${$} (hostname=${X})`);let J={headers:{Authorization:`Bearer ${this.opts.apiKey}`,"x-shogo-hostname":X,"x-shogo-name":z,"x-shogo-os":Y,"x-shogo-arch":Z,"x-shogo-kind":this.opts.kind??"cli-worker"}},K;try{K=this.createTunnelWebSocket($,J)}catch(q){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${q?.message??q}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=K,K.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},K.onmessage=(q)=>{let Q;try{let V=typeof q.data==="string"?q.data:q.data.toString();Q=JSON.parse(V)}catch{return}if(Q.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(Q.type==="cancel"){let V=this.activeAbortControllers.get(Q.requestId);if(V)V.abort();return}if(Q.type==="request"){this.handleRequest(Q).catch((V)=>{this.log.error(`[WorkerTunnel] Error handling request: ${V?.message??V}`)});return}},K.onclose=(q)=>{if(this.log.log(`[WorkerTunnel] WebSocket closed: code=${q.code} reason=${q.reason||"none"}`),this.cleanupWs(),this.stopped)return;if(q.code===1000||q.code===4000)this.scheduleNextPoll(this.currentPollInterval);else{this.wsReconnectAttempt++;let Q=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(Q/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(Q/1000))}},K.onerror=(q)=>{this.log.error("[WorkerTunnel] WebSocket error:",q?.message??"unknown")}}cleanupWs(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer),this.wsIdleTimer=null;for(let[,$]of this.activeAbortControllers)try{$.abort()}catch{}this.activeAbortControllers.clear(),this.ws=null}_testing(){let $=this;return{sendHeartbeat:()=>$.sendHeartbeat(),heartbeatLoop:()=>$.heartbeatLoop(),connectWs:()=>$.connectWs(),cleanupWs:()=>$.cleanupWs(),handleRequest:(X)=>$.handleRequest(X),installFakeWs:(X)=>{$.ws=X},getCloudUrl:()=>$.getCloudUrl(),getWsBaseUrl:()=>$.getWsBaseUrl(),buildWsUrl:()=>$.buildWsUrl(),getReconnectDelayMs:()=>$.getReconnectDelayMs(),supportsWebSocketConstructorHeaders:(X)=>$.supportsWebSocketConstructorHeaders(X),createTunnelWebSocket:(X,Y,Z)=>$.createTunnelWebSocket(X,Y,Z),DEFAULT_POLL_INTERVAL_S:Y0,BACKOFF_BASE_MS:O2,BACKOFF_MAX_MS:B2,TUNNEL_PROTOCOL_VERSION:A2,get currentPollInterval(){return $.currentPollInterval},set currentPollInterval(X){$.currentPollInterval=X},get wsReconnectAttempt(){return $.wsReconnectAttempt},set wsReconnectAttempt(X){$.wsReconnectAttempt=X},get ws(){return $.ws},get stopped(){return $.stopped},get serverPublishedWsUrl(){return $.serverPublishedWsUrl},set serverPublishedWsUrl(X){$.serverPublishedWsUrl=X}}}}async function F2($){let X=_({name:$.name,workerDir:$.workerDir,apiKey:$.apiKey,cloudUrl:$.cloudUrl,port:$.port?parseInt($.port,10):void 0,projectsDir:$.projectsDir}),Y=s0($.proxy);if(!$.foreground)return G6({cfg:X,proxy:Y,flags:$});let Z=u({flag:$.runtimeBin});if(!Z)console.error(G.red(d({flag:$.runtimeBin}))),process.exit(1);if(M0(process.env,Y),$.debug){if(!await Y2(X2({cloudUrl:X.cloudUrl,apiKey:X.apiKey,workerDir:X.workerDir,proxy:Y})))process.exit(1)}let z=!$.noAutoPull,J=!$.noGit;if(console.log(G.bold(`
14
14
  Shogo Worker — Starting`)),console.log(G.dim(" name ")+X.name),console.log(G.dim(" worker-dir ")+X.workerDir),console.log(G.dim(" cloud ")+X.cloudUrl),console.log(G.dim(" runtime ")+`${Z.path} ${G.dim(`(via ${Z.source})`)}`),console.log(G.dim(" auto-pull ")+(z?G.green("on")+G.dim(` → ${X.projectsDir}`):G.yellow("off"))),console.log(G.dim(" sync mode ")+(J?G.green("git")+G.dim(" (falls back to file transport if git is missing)"):G.yellow("files-only"))),$.project)console.log(G.dim(" project ")+$.project);if(Y)console.log(G.dim(" proxy ")+`${Y.url} ${G.dim(`(from ${Y.source})`)}`);console.log("");let K=`${X.cloudUrl.replace(/\/+$/,"")}/api/ai/v1`,q={cloudUrl:X.cloudUrl,apiKey:X.apiKey,aiProxyUrl:K,aiProxyToken:X.apiKey},Q=new v0({runtimeBin:$.runtimeBin,defaultSpawnConfig:q,autoPull:{enabled:z,projectsDir:X.projectsDir,watch:!0,useGit:J}});if(!Q.resolveBinary())console.error(G.red(d({flag:$.runtimeBin}))),process.exit(1);let V=new u0({apiKey:X.apiKey,cloudUrl:X.cloudUrl,name:X.name,kind:"cli-worker",resolver:Q,onAuthRevoked:(W)=>{console.error(G.red(`✗ Cloud auth revoked: ${W}`)),console.error(G.dim(" Run `shogo login` to re-authenticate; this worker will keep polling at the auth-failure backoff until then."))}}),H=!1,w=async(W)=>{if(H)return;H=!0,console.log(G.dim(`
15
15
  Received ${W} — shutting down...`));try{V.stop()}catch{}try{await Q.stopAll()}catch(C){console.warn(G.yellow(`stopAll: ${C?.message??C}`))}process.exit(0)};process.once("SIGINT",()=>void w("SIGINT")),process.once("SIGTERM",()=>void w("SIGTERM")),process.once("SIGHUP",()=>void w("SIGHUP")),V.start(),console.log(G.green("✓ Worker running. Ctrl-C to stop.")),await new Promise(()=>{})}function G6({cfg:$,proxy:X,flags:Y}){let{entry:Z,runner:z}=L6(),J=M0({...process.env,SHOGO_API_KEY:$.apiKey,SHOGO_CLOUD_URL:$.cloudUrl,SHOGO_INSTANCE_NAME:$.name,SHOGO_WORKER_DIR:$.workerDir,SHOGO_LOCAL_MODE:"true",PORT:String($.port)},X),K=C6(Y),{pid:q}=l0({entry:Z,runner:z,env:{...J,SHOGO_DETACHED_ARGS:K.join(" ")},cwd:$.workerDir,detach:!0,inheritStdio:!1});console.log(G.bold(`
16
16
  Shogo Worker — Started`)),console.log(G.dim(" pid: ")+q),console.log(G.dim(" name: ")+$.name),console.log(G.dim(" logs: ")+"~/.shogo/logs/worker.log"),console.log(G.dim(" stop: ")+"shogo worker stop")}function C6($){let X=["worker","start","--foreground"];if($.name)X.push("--name",$.name);if($.workerDir)X.push("--worker-dir",$.workerDir);if($.apiKey)X.push("--api-key",$.apiKey);if($.cloudUrl)X.push("--cloud-url",$.cloudUrl);if($.port)X.push("--port",$.port);if($.proxy)X.push("--proxy",$.proxy);if($.project)X.push("--project",$.project);if($.runtimeBin)X.push("--runtime-bin",$.runtimeBin);if($.debug)X.push("--debug");if($.noAutoPull)X.push("--no-auto-pull");if($.projectsDir)X.push("--projects-dir",$.projectsDir);if($.noGit)X.push("--no-git");return X}function L6(){let $=process.execPath,X=/\bbun(?:-[^/\\]*)?$/.test($)||typeof globalThis.Bun<"u",Y=process.argv[1];if(Y&&k6(Y)){let z=/\.ts$/.test(Y);return{entry:Y,runner:X?"bun":z?"tsx":"node"}}return{entry:new URL("../../bin/shogo.mjs",import.meta.url).pathname,runner:X?"bun":"node"}}import N2 from"picocolors";async function M2(){let{killedPid:$}=r0("SIGTERM");if($===null){console.log(N2.dim("No worker running."));return}console.log(N2.green(`✓ Worker stopped (pid=${$}).`))}import v from"picocolors";async function T2(){let $=Q0();if(!$){console.log(v.yellow("● stopped")+v.dim(" (no pid file)"));return}if(!H0($)){console.log(v.red("● dead")+v.dim(` (stale pid ${$})`));return}let X=I();if(console.log(v.green("● running")+v.dim(` (pid ${$})`)),X.name)console.log(v.dim(" name: ")+X.name);if(X.cloudUrl)console.log(v.dim(" cloud: ")+X.cloudUrl);if(X.workerDir)console.log(v.dim(" dir: ")+X.workerDir)}import{spawn as O6}from"node:child_process";import{existsSync as B6,statSync as A6}from"node:fs";import y2 from"picocolors";async function D2($){let X=$.err?q0:K0;if(!B6(X)){console.log(y2.dim("No logs yet."));return}let Y=$.follow?["-F","-n","200",X]:["-n","200",X];if(O6("tail",Y,{stdio:"inherit"}).on("exit",(z)=>process.exit(z??0)),!$.follow){if(A6(X).size===0)console.log(y2.dim("(log file is empty)"))}}import y from"picocolors";import{spawn as U6}from"node:child_process";import{hostname as F6,platform as N6,arch as M6}from"node:os";import f from"picocolors";class P extends Error{kind;constructor($,X){super($);this.kind=X;this.name="CloudLoginError"}}async function x2($){let X=$.cloudUrl.replace(/\/$/,""),Y=$.client??"cli",Z=$.deviceName??F6(),z=$.devicePlatform??`${N6()}-${M6()}`,J=$.appVersion??D6(),K=$.log??((k)=>console.log(k)),q=$.fetchImpl??fetch,Q=await q(`${X}/api/cli/login/start`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({deviceId:$.deviceId,deviceName:Z,devicePlatform:z,deviceAppVersion:J,workspaceId:$.workspaceId,client:Y}),signal:AbortSignal.timeout(1e4)}).catch((k)=>{throw new P(`Cannot reach Shogo Cloud at ${X}: ${k?.message??k}`,"transport")});if(!Q.ok){let k=await Q.text().catch(()=>"");throw new P(`Cloud rejected /api/cli/login/start (HTTP ${Q.status}): ${k||"no body"}`,"transport")}let V=await Q.json().catch(()=>({}));if(!V?.ok||!V.state||!V.authUrl)throw new P(`Cloud returned a malformed start response: ${V?.error??JSON.stringify(V)}`,"transport");let H=T6($.pollIntervalMs??V.pollIntervalMs),w=$.timeoutMs??V.expiresInMs,W=Date.now()+w;if(K(""),K(f.bold("Sign in to Shogo Cloud")),K(f.dim(" cloud: ")+X),K(f.dim(" device: ")+`${Z} (${z})`),K(f.dim(" user code: ")+f.cyan(V.userCode)),K(""),K(" Open this URL in your browser to approve:"),K(" "+f.cyan(V.authUrl)),K(""),$.openBrowser!==!1)if(typeof $.openBrowser==="function")try{await $.openBrowser(V.authUrl)}catch{}else x6(V.authUrl).catch(()=>{});K(f.dim("Waiting for approval..."));let C=$.installSignalHandlers??!0,O=()=>{A.abort()},A=new AbortController;if($.abortSignal)if($.abortSignal.aborted)A.abort();else $.abortSignal.addEventListener("abort",()=>A.abort(),{once:!0});if(C)process.once("SIGINT",O),process.once("SIGTERM",O);try{while(!0){if(A.signal.aborted)throw new P("Aborted by caller.","cancelled");if(Date.now()>=W)throw new P(`Timed out after ${Math.round(w/1000)}s waiting for approval.`,"timeout");let k=await q(`${X}/api/cli/login/poll?state=${encodeURIComponent(V.state)}`,{method:"GET",headers:{accept:"application/json"},signal:AbortSignal.timeout(1e4)}).catch((M)=>{return K(f.dim(` (poll error: ${M?.message??M} — retrying)`)),null});if(k&&k.ok){let M=await k.json().catch(()=>({}));if(M?.status==="approved"&&M.key)return{key:M.key,email:M.email??null,workspace:M.workspace??null,deviceId:M.deviceId??$.deviceId};if(M?.status==="denied")throw new P("Sign-in was denied in the browser.","denied");if(M?.status==="expired")throw new P("Sign-in request expired before it was approved.","expired")}await y6(H,A.signal)}}finally{if(C)process.removeListener("SIGINT",O),process.removeListener("SIGTERM",O)}}function T6($){if(!Number.isFinite($)||$<=0)return 2000;return Math.min(Math.max($,1000),1e4)}function y6($,X){return new Promise((Y,Z)=>{if(X?.aborted)return Z(new P("Aborted by caller.","cancelled"));let z=setTimeout(()=>{X?.removeEventListener("abort",J),Y()},$),J=()=>{clearTimeout(z),Z(new P("Aborted by caller.","cancelled"))};X?.addEventListener("abort",J,{once:!0})})}function D6(){if(typeof __SHOGO_WORKER_VERSION__==="string"&&__SHOGO_WORKER_VERSION__.length>0)return`shogo-cli/${__SHOGO_WORKER_VERSION__}`;return"shogo-cli/unknown"}function x6($){return new Promise((X)=>{let Y=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",Z=process.platform==="win32"?["/c","start",'""',$]:[$];try{let z=U6(Y,Z,{stdio:"ignore",detached:!0});z.on("error",()=>X()),z.unref(),X()}catch{X()}})}import{readFileSync as P6,writeFileSync as S6,existsSync as R6,chmodSync as _6}from"node:fs";import{randomUUID as E6}from"node:crypto";function P2(){if(R6(p)){let X=P6(p,"utf-8").trim();if(X)return X}j();let $=E6();return S6(p,$,{mode:384}),_6(p,384),$}var b6="https://studio.shogo.ai";async function S2($){let X=I(),Y=($.cloudUrl||X.cloudUrl||b6).replace(/\/$/,""),Z=$.apiKey||process.env.SHOGO_API_KEY;if(Z){await v6({key:Z,cloudUrl:Y,cfg:X,name:$.name});return}let z=P2(),J;try{J=await x2({cloudUrl:Y,deviceId:z,deviceName:$.name,workspaceId:$.workspace,openBrowser:!$.noBrowser})}catch(K){if(K instanceof P){if(console.error(y.red(`✗ ${K.message}`)),K.kind==="transport")console.error(y.dim(" If your network blocks browsers, run with --api-key <key> instead."));process.exit(1)}throw K}if(X.apiKey=J.key,X.cloudUrl=Y,$.name)X.name=$.name;if(l(X),console.log(""),console.log(y.green("✓ Signed in to Shogo Cloud")),J.workspace)console.log(y.dim(" workspace: ")+J.workspace);if(J.email)console.log(y.dim(" email: ")+J.email);console.log(y.dim(" saved to: ")+"~/.shogo/config.json"),console.log(""),console.log(y.dim(" next: ")+"shogo worker start")}async function v6({key:$,cloudUrl:X,cfg:Y,name:Z}){if(!/^shogo_sk_/.test($))console.error(y.red('✗ API key should start with "shogo_sk_". Copy it verbatim from the API Keys page.')),process.exit(1);let z;try{let J=await fetch(`${X}/api/api-keys/validate`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({key:$}),signal:AbortSignal.timeout(1e4)});if(z=await J.json().catch(()=>({})),!J.ok||!z?.valid)console.error(y.red(`✗ ${z?.error||`Cloud rejected the key (HTTP ${J.status}).`}`)),process.exit(1)}catch(J){console.error(y.red(`✗ Cannot reach Shogo Cloud at ${X}: ${J?.message??J}`)),process.exit(1)}if(Y.apiKey=$,Y.cloudUrl=X,Z)Y.name=Z;if(l(Y),console.log(y.green("✓ API key saved to ~/.shogo/config.json")),z.workspace?.name)console.log(y.dim(" workspace: ")+z.workspace.name);if(z.user?.email)console.log(y.dim(" email: ")+z.user.email);console.log(y.dim(" next: ")+"shogo worker start")}import Z0 from"picocolors";async function R2(){let $=I();if(Object.keys($).length===0){console.log(Z0.dim("(no config — run `shogo login` or `shogo config set`)"));return}let X={...$,apiKey:$.apiKey?`***${$.apiKey.slice(-4)}`:void 0};console.log(Z0.dim(`file: ${h}`)),console.log(JSON.stringify(X,null,2))}async function _2($,X){let Y=["apiKey","cloudUrl","name","workerDir","port"];if(!Y.includes($))console.error(Z0.red(`Unknown key: ${$}`)),console.error(Z0.dim(`Allowed: ${Y.join(", ")}`)),process.exit(1);let Z=I();Z[$]=$==="port"?parseInt(X,10):X,l(Z),console.log(Z0.green(`✓ ${$} set`))}import U from"picocolors";import{spawn as m6}from"node:child_process";import{createHash as h6}from"node:crypto";import{createWriteStream as j6,existsSync as z0,mkdirSync as f0,readFileSync as v2,renameSync as E2,rmSync as d0,writeFileSync as I6}from"node:fs";import{tmpdir as u6}from"node:os";import{dirname as f6,join as C0}from"node:path";import{Readable as d6}from"node:stream";import{pipeline as n6}from"node:stream/promises";var i6="https://github.com/shogo-labs/shogo-ai/releases/download";function m2(){let{platform:$,arch:X}=process,Y;if($==="darwin")Y="darwin";else if($==="linux")Y="linux";else if($==="win32")Y="windows";else throw Error(`Unsupported platform: ${$}`);let Z;if(X==="arm64")Z="arm64";else if(X==="x64")Z="x64";else throw Error(`Unsupported arch: ${X} (need arm64 or x64)`);return`${Y}-${Z}`}function L0(){if(!z0(a))return null;try{let $=v2(a,"utf-8");return JSON.parse($)}catch{return null}}async function g6($,X){let Y=X.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/releases\/download/);if(!Y)throw Error(`Cannot auto-resolve latest version from non-GitHub baseUrl '${X}'. Pass --version explicitly.`);let Z=Y[1],z=Y[2];if($==="stable"){let H=`https://api.github.com/repos/${Z}/${z}/releases/latest`,w=await fetch(H,{headers:{Accept:"application/vnd.github+json"}});if(!w.ok)throw Error(`GitHub API ${w.status} for ${H}`);let W=await w.json();if(!W.tag_name)throw Error("GitHub API did not return tag_name");return b2(W.tag_name)}let J=`https://api.github.com/repos/${Z}/${z}/releases?per_page=30`,K=await fetch(J,{headers:{Accept:"application/vnd.github+json"}});if(!K.ok)throw Error(`GitHub API ${K.status} for ${J}`);let q=await K.json(),Q=$==="beta"?"-beta":"-nightly",V=q.find((H)=>H.prerelease&&/^v\d+\.\d+\.\d+-/.test(H.tag_name)&&H.tag_name.includes(Q));if(!V)throw Error(`No ${$} runtime release found`);return b2(V.tag_name)}function b2($){if(!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test($))throw Error(`Unexpected app tag '${$}' (expected vX.Y.Z[-prerelease])`);return $.slice(1)}function p6($,X,Y){let Z=`shogo-agent-runtime-${X}.tar.gz`,z=`v${$}`,J=`${Y.replace(/\/$/,"")}/${z}/${Z}`;return{tarball:J,sha256:`${J}.sha256`,assetName:Z}}async function c6($,X){let Y=await fetch($,{redirect:"follow"});if(!Y.ok)throw Error(`Download failed: HTTP ${Y.status} for ${$}`);if(!Y.body)throw Error(`Download failed: empty body for ${$}`);await n6(d6.fromWeb(Y.body),j6(X))}async function o6($){let X=await fetch($,{redirect:"follow"});if(!X.ok)throw Error(`SHA256 sidecar fetch failed: HTTP ${X.status} for ${$}`);let Z=(await X.text()).trim().split(/\s+/)[0];if(!/^[0-9a-f]{64}$/i.test(Z))throw Error(`SHA256 sidecar at ${$} did not contain a 64-char hex digest`);return Z.toLowerCase()}function a6($){let X=v2($);return h6("sha256").update(X).digest("hex")}async function l6($,X){await new Promise((Y,Z)=>{let z=m6("tar",["-xzf",$,"-C",X],{stdio:["ignore","pipe","pipe"]}),J="";z.stderr.on("data",(K)=>{J+=K.toString()}),z.on("error",Z),z.on("exit",(K)=>{if(K===0)Y();else Z(Error(`tar exited ${K}: ${J.trim()}`))})})}function r6($,X){p0();let Y=f6(X);if(!z0(Y))f0(Y,{recursive:!0,mode:448});let Z=`${X}.next`;if(z0(Z))d0(Z,{force:!0});E2($,Z);try{if(process.platform!=="win32"){let{chmodSync:z}=Q$("node:fs");z(Z,493)}}catch{}if(z0(X))d0(X,{force:!0});E2(Z,X)}async function h2($={}){let X=$.logger??console,Y=$.channel??"stable",Z=$.baseUrl??process.env.SHOGO_RUNTIME_RELEASES_URL??i6,z=$.target??m2(),J=$.version;if(!J)X.log(`[runtime install] Resolving latest ${Y} version...`),J=await g6(Y,Z),X.log(`[runtime install] Latest ${Y} = ${J}`);let K=L0();if(K&&K.version===J&&K.target===z&&!$.force)return X.log(`[runtime install] ${J} (${z}) already installed at ${R} — pass --force to reinstall`),{version:J,target:z,binPath:R,source:K.source,sha256:K.sha256,channel:Y};let q=p6(J,z,Z);X.log(`[runtime install] Downloading ${q.tarball}`);let Q=C0(u6(),`shogo-runtime-install-${process.pid}-${Date.now()}`);f0(Q,{recursive:!0});try{let V=C0(Q,q.assetName);await c6(q.tarball,V),X.log("[runtime install] Verifying SHA-256...");let H=await o6(q.sha256),w=a6(V);if(w!==H)throw Error(`SHA-256 mismatch for ${q.assetName}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shogo-ai/worker",
3
- "version": "1.10.1",
3
+ "version": "1.10.3",
4
4
  "description": "Shogo Cloud Agent Worker — run Shogo agents on your own machine (laptop, devbox, CI).",
5
5
  "license": "MIT",
6
6
  "author": "Shogo Technologies, Inc.",
@@ -19,6 +19,8 @@
19
19
  ".": "./src/cli.ts",
20
20
  "./tunnel": "./src/lib/tunnel.ts",
21
21
  "./runtime-manager": "./src/lib/runtime-manager.ts",
22
+ "./cloud-sync-watcher": "./src/lib/cloud-sync-watcher.ts",
23
+ "./git-cloner": "./src/lib/git-cloner.ts",
22
24
  "./runtime-resolver": "./src/lib/runtime-resolver.ts",
23
25
  "./runtime-install": "./src/lib/runtime-install.ts",
24
26
  "./cloud-login": "./src/lib/cloud-login.ts",
@@ -0,0 +1,119 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * Tests for `WorkerRuntimeManager.shouldDeferToCloudSync` — the gate that
5
+ * decides whether a spawned runtime gets `SHOGO_CLOUD_SYNC=1` (skip its own
6
+ * S3Sync + checkpoint inserts because the worker's CloudSyncWatcher owns the
7
+ * push back to cloud).
8
+ *
9
+ * The gate must be true ONLY for projects that were actually auto-pulled, not
10
+ * for every project when `autoPull.enabled` is set globally — otherwise a
11
+ * project served from a pre-seeded / template `projectDir` (which has no
12
+ * watcher) would wrongly suppress its own checkpoints.
13
+ *
14
+ * bun test packages/shogo-worker/src/lib/__tests__/runtime-manager-cloud-sync-gate.test.ts
15
+ */
16
+
17
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
18
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
19
+ import { tmpdir } from 'node:os';
20
+ import { join } from 'node:path';
21
+
22
+ let scriptedFetch: typeof fetch | null = null;
23
+ const originalFetch = globalThis.fetch;
24
+ beforeEach(() => {
25
+ globalThis.fetch = ((input: any, init?: RequestInit) => {
26
+ if (!scriptedFetch) throw new Error('no scripted fetch set');
27
+ return scriptedFetch(input, init);
28
+ }) as any;
29
+ });
30
+ afterEach(() => {
31
+ globalThis.fetch = originalFetch;
32
+ scriptedFetch = null;
33
+ });
34
+
35
+ /** Manifest + presign + download responses for a files-mode pull. */
36
+ function scriptManifest(files: Array<{ path: string; size: number; content: string }>) {
37
+ return (async (input: any, init?: RequestInit) => {
38
+ const url = typeof input === 'string' ? input : input.url;
39
+ if (url.includes('/workspace/manifest')) {
40
+ return new Response(
41
+ JSON.stringify({
42
+ ok: true,
43
+ projectId: 'p',
44
+ source: 's3',
45
+ generatedAt: '',
46
+ files: files.map((f) => ({ path: f.path, size: f.size, lastModified: null, etag: null })),
47
+ }),
48
+ { status: 200, headers: { 'content-type': 'application/json' } },
49
+ );
50
+ }
51
+ if (url.includes('/s3/presign')) {
52
+ const body = JSON.parse((init?.body as string) || '{}') as { files: Array<{ path: string; action: string }> };
53
+ return new Response(
54
+ JSON.stringify({ ok: true, urls: body.files.map((f) => ({ path: f.path, action: f.action, url: `https://dl.test/${f.path}` })) }),
55
+ { status: 200 },
56
+ );
57
+ }
58
+ if (url.startsWith('https://dl.test/')) {
59
+ const path = url.slice('https://dl.test/'.length);
60
+ const file = files.find((f) => f.path === path);
61
+ return file ? new Response(file.content, { status: 200 }) : new Response('nf', { status: 404 });
62
+ }
63
+ return new Response('unhandled', { status: 500 });
64
+ }) as unknown as typeof fetch;
65
+ }
66
+
67
+ describe('WorkerRuntimeManager.shouldDeferToCloudSync', () => {
68
+ let dir: string;
69
+ beforeEach(() => {
70
+ dir = mkdtempSync(join(tmpdir(), 'wrm-cloud-sync-gate-'));
71
+ });
72
+ afterEach(() => {
73
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
74
+ });
75
+
76
+ it('is true only for a project that was actually auto-pulled', async () => {
77
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
78
+ scriptedFetch = scriptManifest([{ path: 'a.ts', size: 1, content: 'A' }]);
79
+
80
+ const mgr = new WorkerRuntimeManager({
81
+ autoPull: { enabled: true, projectsDir: dir, watch: false, useGit: false },
82
+ });
83
+
84
+ // Before any pull: not active.
85
+ expect(mgr.shouldDeferToCloudSync('proj-pulled')).toBe(false);
86
+
87
+ await mgr.ensurePulled('proj-pulled', { cloudUrl: 'https://api.test', apiKey: 'shogo_sk_x' });
88
+
89
+ // Pulled → defer to cloud sync.
90
+ expect(mgr.shouldDeferToCloudSync('proj-pulled')).toBe(true);
91
+ // A different, never-pulled project → keep its own sync/checkpoints.
92
+ expect(mgr.shouldDeferToCloudSync('proj-untouched')).toBe(false);
93
+ });
94
+
95
+ it('is false for a pre-seeded projectDir even when autoPull is enabled (no watcher)', async () => {
96
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
97
+ // No fetch should fire — the caller-provided projectDir short-circuits pull.
98
+ scriptedFetch = (async () => { throw new Error('no fetch expected'); }) as unknown as typeof fetch;
99
+
100
+ const target = join(dir, 'pre-seeded');
101
+ mkdirSync(target, { recursive: true });
102
+ writeFileSync(join(target, 'package.json'), '{}');
103
+
104
+ const mgr = new WorkerRuntimeManager({
105
+ autoPull: { enabled: true, projectsDir: dir, watch: false, useGit: false },
106
+ });
107
+ await mgr.ensurePulled('pre-seeded', { cloudUrl: 'https://api.test', apiKey: 'shogo_sk_x', projectDir: target });
108
+
109
+ // maybeAutoPull returned early (projectDir existed) so the project was
110
+ // never added to pulledProjects → it keeps its own checkpoint behavior.
111
+ expect(mgr.shouldDeferToCloudSync('pre-seeded')).toBe(false);
112
+ });
113
+
114
+ it('is false when autoPull is disabled entirely', async () => {
115
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
116
+ const mgr = new WorkerRuntimeManager({ autoPull: { enabled: false, projectsDir: dir } });
117
+ expect(mgr.shouldDeferToCloudSync('anything')).toBe(false);
118
+ });
119
+ });
@@ -977,6 +977,19 @@ export class WorkerRuntimeManager implements RuntimeResolver {
977
977
  }
978
978
  }
979
979
 
980
+ /**
981
+ * Whether the spawned runtime for `projectId` should defer its own
982
+ * S3Sync + checkpoint inserts to the worker's cloud sync (sets
983
+ * `SHOGO_CLOUD_SYNC=1`). True only when auto-pull is enabled AND this
984
+ * project was actually pulled (so a watcher is running for it) — a
985
+ * pre-seeded / operator-pinned / template-seeded project keeps the
986
+ * runtime's own sync. Public so the wiring is unit-testable without
987
+ * spawning a runtime.
988
+ */
989
+ shouldDeferToCloudSync(projectId: string): boolean {
990
+ return !!this.opts.autoPull?.enabled && this.pulledProjects.has(projectId);
991
+ }
992
+
980
993
  status(projectId: string): RuntimeStatusInfo | null {
981
994
  const r = this.runtimes.get(projectId);
982
995
  return r ? this.snapshot(r) : null;
@@ -1295,7 +1308,14 @@ export class WorkerRuntimeManager implements RuntimeResolver {
1295
1308
  // already running a CloudFileTransport watcher against this WORKSPACE_DIR.
1296
1309
  // Without this both sides upload the same files and the watcher loops on
1297
1310
  // its own writes.
1298
- if (this.opts.autoPull?.enabled) {
1311
+ //
1312
+ // Gate on whether THIS project was actually auto-pulled, not just on the
1313
+ // global `autoPull.enabled` flag: a worker can serve a pre-pulled or
1314
+ // operator-pinned `projectDir` that auto-pull skipped (line 777), and a
1315
+ // desktop-style embed enables auto-pull globally but still seeds some
1316
+ // projects from a local template. Those projects have no watcher, so they
1317
+ // must keep the runtime's own S3Sync/checkpoint behavior.
1318
+ if (this.shouldDeferToCloudSync(slot.projectId)) {
1299
1319
  env.SHOGO_CLOUD_SYNC = '1';
1300
1320
  }
1301
1321
  if (cfg.aiProxyUrl) env.AI_PROXY_URL = cfg.aiProxyUrl;