grix-connector 4.9.1 → 4.9.2-beta.2
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/adapter/claude/claude-adapter.js +13 -13
- package/dist/adapter/claude/usage-parser.js +8 -7
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.9.2-beta.2.tgz +0 -0
- package/dist/assets/dsh-bridge/manifest.json +6 -6
- package/dist/grix.js +8 -8
- package/dist/launcher.mjs +103 -1
- package/dist/runtime/daemon-lock.js +2 -2
- package/dist/service/platform-adapter.js +30 -30
- package/dist/service/process-control.js +1 -1
- package/dist/service/service-manager.js +3 -3
- package/openclaw-plugin/index.js +1 -1
- package/package.json +1 -1
- package/scripts/launcher.mjs +103 -1
- package/dist/adapter/claude/claude-bridge-server.js +0 -1
- package/dist/adapter/claude/claude-tools.js +0 -1
- package/dist/adapter/claude/claude-worker-client.js +0 -1
- package/dist/adapter/claude/mcp-http-launcher.js +0 -2
- package/dist/adapter/claude/result-timeout.js +0 -1
- package/dist/adapter/deepseek/deepseek-adapter.js +0 -6
- package/dist/adapter/deepseek/index.js +0 -1
- package/dist/adapter/qwen/index.js +0 -1
- package/dist/adapter/qwen/qwen-adapter.js +0 -4
- package/dist/aibot/client.js +0 -1
- package/dist/aibot/index.js +0 -1
- package/dist/aibot/types.js +0 -0
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.9.1.tgz +0 -0
- package/dist/core/access/allowlist-gate.js +0 -1
- package/dist/core/access/allowlist-store.js +0 -1
- package/dist/core/access/index.js +0 -1
- package/dist/core/file-ops/handler.js +0 -1
- package/dist/core/file-ops/list-files.js +0 -1
- package/dist/core/file-ops/types.js +0 -0
- package/dist/log.js +0 -3
- package/dist/main.js +0 -31
- package/dist/mcp/stream-http/config.js +0 -1
- package/dist/mcp/stream-http/connection-binding.js +0 -1
- package/dist/mcp/stream-http/event-tool-executor.js +0 -1
- package/dist/mcp/stream-http/gateway.js +0 -1
- package/dist/mcp/stream-http/index.js +0 -1
- package/dist/mcp/stream-http/security.js +0 -1
- package/dist/mcp/stream-http/session-manager.js +0 -1
- package/dist/mcp/stream-http/tool-executor.js +0 -1
- package/dist/mcp/stream-http/tool-registry.js +0 -1
- package/dist/mcp/stream-http/tool-schemas.js +0 -1
- package/dist/session/index.js +0 -1
- package/dist/session/manager.js +0 -1
- package/dist/transport/index.js +0 -1
- package/dist/transport/json-rpc.js +0 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import{closeSync as
|
|
2
|
-
`)
|
|
3
|
-
`)
|
|
4
|
-
`)
|
|
5
|
-
`)
|
|
6
|
-
`)
|
|
7
|
-
`)
|
|
1
|
+
import{closeSync as F,createReadStream as I,existsSync as M,openSync as v,readdirSync as X,readFileSync as E,readSync as H,realpathSync as q,statSync as N}from"node:fs";import{createInterface as Q}from"node:readline";import b from"node:path";import D from"node:os";import{log as j}from"../../core/log/index.js";function V(t){let e=t;try{e=q(t)}catch{}return e.replace(/[^a-zA-Z0-9]/g,"-")}function Y(t,e){const n=b.join(D.homedir(),".claude"),s=b.join(n,"projects",V(e));return b.join(s,`${t}.jsonl`)}const Z=5e3,J=64,d=new Map;let C=0;const m=new Map;function G(t,e){for(d.delete(t),d.set(t,e);d.size>J;){const n=d.keys().next();if(n.done)break;d.delete(n.value)}}function it(t){t===void 0?(d.clear(),m.clear(),C=0):(d.delete(t),m.delete(t))}function ct(t,e){if(!(!t||!e)&&b.isAbsolute(e)&&b.basename(e)===`${t}.jsonl`&&m.get(t)!==e)for(m.delete(t),m.set(t,e);m.size>J;){const n=m.keys().next();if(n.done)break;m.delete(n.value)}}function K(t){const e=b.join(D.homedir(),".claude","projects"),n=`${t}.jsonl`;let s;try{s=X(e,{withFileTypes:!0})}catch{return null}let o=null;for(const r of s){if(!r.isDirectory())continue;const c=b.join(e,r.name,n);try{const i=N(c);if(!i.isFile()||i.size===0)continue;(!o||i.size>o.size)&&(o={path:c,size:i.size})}catch{}}return o?.path??null}function _(t,e){const n=m.get(t);if(n&&M(n))return n;const s=Y(t,e);if(M(s))return d.delete(t),s;const o=d.get(t);if(o&&M(o))return o;const r=Date.now();if(r-C<Z)return s;C=r;const c=K(t);return G(t,c),c?(j.warn("usage-parser",`Session ${t} transcript drifted off the bound cwd: reading ${c} instead of ${s}`),c):s}function P(){return{inputTokens:0,outputTokens:0,cacheReadInputTokens:0,cacheCreationInputTokens:0}}function z(t,e){t.inputTokens+=e.input_tokens??0,t.outputTokens+=e.output_tokens??0,t.cacheReadInputTokens+=e.cache_read_input_tokens??0,t.cacheCreationInputTokens+=e.cache_creation_input_tokens??0}async function at(t,e){const n=_(t,e);j.info("usage-parser",`Parsing session usage from ${n}`);const s=new Map,o=P();let r=0;try{const i=Q({input:I(n,"utf8"),crlfDelay:1/0});for await(const u of i)if(u.trim())try{const f=JSON.parse(u);if(f.type!=="assistant")continue;const a=f.message?.usage;if(!a)continue;const l=f.message?.model??"unknown";r++,z(o,a);let p=s.get(l);p||(p={turns:0,usage:P()},s.set(l,p)),p.turns++,z(p.usage,a)}catch{}}catch(i){return i.code==="ENOENT"?(j.info("usage-parser",`Session JSONL not found: ${n}`),null):(j.error("usage-parser",`Failed to parse session usage: ${i}`),null)}return r===0?null:{models:[...s.entries()].map(([i,u])=>({model:i,turns:u.turns,total:u.usage})),total:o,turns:r}}const x=64*1024,W=9e4,tt=new Set(["No response requested."]);function L(t){return tt.has(t.trim())}function $(t){return t.isApiErrorMessage===!0}function B(t){const e=t.message;return e?.content?(Array.isArray(e.content)?e.content:[{type:"text",text:String(e.content)}]).map(s=>s).filter(s=>s.type==="text"&&s.text?.trim()).map(s=>s.text.trim()).join(`
|
|
2
|
+
`):""}function lt(t,e,n){const s=_(t,e);try{const o=E(s);let r;n!==void 0&&n>0?r=n<o.length?o.subarray(n):Buffer.alloc(0):r=o.length>x?o.subarray(o.length-x):o;const c=r.toString("utf8").split(`
|
|
3
|
+
`);let i;for(let u=c.length-1;u>=0;u--){const f=c[u].trim();if(!f)continue;let a;try{a=JSON.parse(f)}catch{continue}if(a.type!=="assistant")continue;const l=a.message;if($(a))return{text:"",stopReason:i!==void 0?i:l?.stop_reason??null,apiError:!0,apiErrorText:B(a)};if(!l?.content)continue;const p=Array.isArray(l.content)?l.content:[{type:"text",text:String(l.content)}],y=[];let h=0;for(const S of p){const g=S;if(!(g.type!=="text"||!g.text?.trim())){if(L(g.text)){h++;continue}y.push(g.text)}}if(y.length>0)return{text:y.join(`
|
|
4
|
+
`),stopReason:i!==void 0?i:l.stop_reason??null};h>0&&i===void 0&&(i=l.stop_reason??null)}return i!==void 0?{text:"",stopReason:i,metaOnly:!0}:null}catch{return null}}function ut(t,e,n){const s=_(t,e),o=n>0?n:0;let r;try{const c=N(s).size;if(o>=c)return{segments:[],newOffset:o};const i=c-o,u=Buffer.alloc(i);r=v(s,"r");const f=H(r,u,0,i,o),a=u.subarray(0,f);let l=-1;for(let T=a.length-1;T>=0;T--)if(a[T]===10){l=T;break}if(l<0)return{segments:[],newOffset:o};const p=a.subarray(0,l+1),y=o+p.length,h=[];let S=!1,g="";for(const T of p.toString("utf8").split(`
|
|
5
|
+
`)){const k=T.trim();if(!k)continue;let A;try{A=JSON.parse(k)}catch{continue}if(A.type!=="assistant")continue;if($(A)){S=!0,g=B(A)||g;continue}const R=A.message;if(!R?.content)continue;const O=Array.isArray(R.content)?R.content:[{type:"text",text:String(R.content)}];for(const U of O){const w=U;w.type!=="text"||!w.text?.trim()||L(w.text)||h.push(w.text)}}return{segments:h,newOffset:y,apiError:S,apiErrorText:g}}catch{return{segments:[],newOffset:o}}finally{if(r!==void 0)try{F(r)}catch{}}}function ft(t,e,n){const s=_(t,e);let o=null,r;try{const u=N(s);o=Date.now()-u.mtimeMs,r=E(s)}catch{return{lastStopReason:null,freshMs:null}}let c;n!==void 0&&n>0?c=n<r.length?r.subarray(n):Buffer.alloc(0):c=r.length>x?r.subarray(r.length-x):r;const i=c.toString("utf8").split(`
|
|
6
|
+
`);for(let u=i.length-1;u>=0;u--){const f=i[u].trim();if(!f)continue;let a;try{a=JSON.parse(f)}catch{continue}if(a.type!=="assistant")continue;const l=a.message;if(l)return{lastStopReason:l.stop_reason??null,freshMs:o}}return{lastStopReason:null,freshMs:o}}function pt(t,e){const n=_(t,e);let s;try{s=E(n)}catch{return 0}const r=(s.length>x?s.subarray(s.length-x):s).toString("utf8").split(`
|
|
7
|
+
`),c=new Set,i=new Set,u=/<task-id>\s*([^<\s]+)\s*<\/task-id>/;for(const a of r){const l=a.trim();if(!l)continue;if(l.includes("task-notification")){const h=u.exec(l);h&&i.add(h[1]);continue}let p;try{p=JSON.parse(l)}catch{continue}const y=p.toolUseResult;y&&typeof y.backgroundTaskId=="string"&&c.add(y.backgroundTaskId)}let f=0;for(const a of c)i.has(a)||f++;return f}function yt(t,e,n,s=!1){const o=_(t,e);try{const r=N(o);if(s&&Date.now()-r.mtimeMs<W)return!1;const c=E(o),u=(n>0&&n<c.length?c.subarray(n):c.length>x?c.subarray(c.length-x):c).toString("utf8").split(`
|
|
8
|
+
`);let f=!1,a=!1;for(const l of u){const p=l.trim();if(!p)continue;let y;try{y=JSON.parse(p)}catch{continue}if(y.type==="user"){const S=y.message?.content??y.content;(Array.isArray(S)?S:[]).some(k=>typeof k=="object"&&k!==null&&k.type==="tool_result")&&(f=!0,a=!1)}else y.type==="assistant"&&f&&(a=!0)}return f&&!a}catch{return!1}}export{it as clearSessionJsonlDriftCache,pt as countPendingBackgroundTasks,V as encodeProjectPath,lt as extractLastAssistantEntry,ut as extractTextBlocksAfterOffset,yt as hasTerminalToolResultAfterOffset,ct as noteSessionJsonlPath,at as parseClaudeSessionUsage,ft as probeSessionTurnState,Y as resolveBoundSessionJsonlPath,_ as resolveSessionJsonlPath};
|
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bridgeVersion": "4.9.
|
|
4
|
-
"tarball": "grix-dsh-bridge-4.9.
|
|
5
|
-
"size":
|
|
6
|
-
"unpackedSize":
|
|
7
|
-
"shasum": "
|
|
8
|
-
"integrity": "sha512-
|
|
3
|
+
"bridgeVersion": "4.9.2-beta.2",
|
|
4
|
+
"tarball": "grix-dsh-bridge-4.9.2-beta.2.tgz",
|
|
5
|
+
"size": 23140,
|
|
6
|
+
"unpackedSize": 92726,
|
|
7
|
+
"shasum": "043ac61a106ba3e1000b1bfda8899ac4238bb2a9",
|
|
8
|
+
"integrity": "sha512-gJe9ismowiHTI7dh+yb/dt4yiiOMTxvWXr3MRDz2oTGg3Zbn3hpf3CFzpN39d4pV7tNMq0qyE0JJ+xFVk5txbg=="
|
|
9
9
|
}
|
package/dist/grix.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
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 c=Y(s.daemonLockFile);c?.pid===process.pid&&c.instance_id===T&&await G(s.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(s.daemonLockFile).catch(()=>{}),H(),process.exit(1)}async function C(t){if(B)return;B=!0,i.info("main",`Received ${t}, shutting down...`),M.markShuttingDown();const c=V();c&&le(c);const m=t==="upgrade-switch"?He:t==="launcher-unhealthy"?Fe:null,g=m??2,l=t==="uncaughtException"||t==="unhandledRejection",h=setTimeout(()=>{i.error("main","Shutdown timed out, forcing exit"),F(s.daemonLockFile).catch(()=>{}),H(),process.exit(g)},1e4);try{await o.stop(),await f()?.stop().catch(()=>{}),await x.stop(),await M.stop(),await J(),await F(s.daemonLockFile),await Je(s.daemonStatusFile).catch(()=>{}),clearTimeout(h),H(),i.info("main","Shutdown complete"),process.exit(m??(l?2:0))}catch(b){i.error("main",`Shutdown error: ${b}`),F(s.daemonLockFile).catch(()=>{}),H(),process.exit(g)}}async function wt(){Ce(),_e(),ke(s.stdoutLogFile,s.stderrLogFile),ie(!1);try{T=(await Xe(s.daemonLockFile,s.rootDir,2,{isHolderServing:Ve(s.dataDir)})).instance_id}catch(e){e instanceof qe&&yt(e.holder),console.error(e instanceof Error?e.message:e),process.exit(1)}if(je()){const e=Ke(s.rootDir,"win32");await Ge(e,{platform:"win32"})}await nt(),$e(),i.info("main",`grix-connector starting (PID ${process.pid})`);try{Le({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(s.daemonStatusFile,{state:"starting",pid:process.pid,instance_id:T,updated_at:Date.now()}),process.env.GRIX_LAUNCHER_MANAGED!=="1"&&await new ce({cliPath:v.resolve(process.argv[1]??""),nodePath:process.execPath}).adoptLauncher(s.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:s.rootDir,requestGracefulShutdown:()=>{C("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:s.rootDir,requestGracefulShutdown:()=>{C("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}`)})},Ne).unref();const c=parseInt(E["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await se({label:"health",port:c,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:n=>M.start(n)});e&&await Se(e)}const m=v.join(s.dataDir,"health-port");ae(m,String(c),"utf-8"),process.on("SIGINT",()=>C("SIGINT")),process.on("SIGTERM",()=>C("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 g="",l=0,h;process.on("uncaughtException",e=>{const n=e instanceof Error?e.stack??e.message:String(e);n===g?(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`),g=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`),g="",l=0,h=void 0},1e4).unref())),!Re(e)&&(z(e,"uncaughtException"),j("uncaughtException",e),C("uncaughtException"))}),process.on("unhandledRejection",e=>{i.error("main",`Unhandled rejection: ${e}`),!Re(e)&&(z(e,"unhandledRejection"),j("unhandledRejection",e),C("unhandledRejection"))});const b=ot(s.dataDir);await b.clearRuntimeState();const _=new at(v.join(s.dataDir,"proxy","relay-state.json"),{warn:e=>i.warn("main",e)}),N=new it(v.join(s.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:_,secrets:N}),U=b.getRelayAgents();if(U.length>0)try{await b.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 L=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&&ue(r,$,_),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 st,A={status:()=>n(),setAgentRelay:async(r,a,d)=>{const u=f(),R={info:w=>i.info("main",w),warn:w=>i.warn("main",w)},S=w=>o.getAgentsStatus().find(D=>D.name===w),y=S(r);if(!a){p.cancel(r);const w=await p.runExclusive(r,async()=>{const D=await Q(W(),R,{agentName:r,target:"off"});return{...n(),restarted:D.restarted,busy:D.busy,...D.pending?{pending:D.pending}:{}}});return o.reportRelayStateLocalChange(r),w}if(!y)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});if(!Oe(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),D=S(r);if(!D)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});const re=String(D.agentId??"").trim();if(!re)throw new me(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const ne=p.waitCancelled(r,w);let k;try{k=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:De,busy:ve,pending:oe}=await ge(u,o,S,R,{localName:r,agentId:re,virtualKey:k.apiKey,anthropicBaseUrl:k.anthropicBaseUrl,openaiBaseUrl:k.openaiBaseUrl,model:d,...k.directRelay!==void 0?{directRelay:k.directRelay}:{}},(xe,be,Pe)=>o.setAgentProvider(xe,be,{deferRestart:Pe?.deferRestart===!0}),{store:_,secrets:N,direct:$});if(p.epoch(r)!==w)throw await Q(W(),R,{agentName:r,target:"off"}),new fe("relay enable cancelled by a later disable","CANCELLED");return{...n(),restarted:De,busy:ve,...oe?{pending:oe}:{}}});return o.reportRelayStateLocalChange(r),O},setAgentRelayCredential:async(r,a)=>{const d=f(),u=o.getAgentsStatus(),{restarted:R,busy:S,pending:y}=await ge(d,o,P=>u.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:$});return o.reportRelayStateLocalChange(r),{...n(),restarted:R,busy:S,...y?{pending:y}:{}}},disableAll:async()=>{const r=f(),a={info:u=>i.info("main",u),warn:u=>i.warn("main",u)},d=new Set([...r.getRelayAgents(),...o.getDirectRelayConfigAgentNames(),...Object.keys(_.list())]);for(const u of d)p.cancel(u);for(const u of d)await p.runExclusive(u,()=>Q(W(),a,{agentName:u,target:"off"})),o.reportRelayStateLocalChange(u);return await pe(r,o),n()},setRoute:async(r,a)=>{const d=a,u=f();return u.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 u.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()}};L=A,x.setProxyHandler(A),o.setRelayStateApplier({getLocalState:r=>{const a=f();if(a.getRelayAgents().includes(r)){const u=o.getAgentsStatus().find(y=>y.name===r),R=`grix-gateway-${String(u?.clientType??"").trim().toLowerCase()}`;return{enabled:!0,model:a.getConfigSnapshot().routes?.find(y=>y.routeKey===R)?.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 u=o.getAgentsStatus().find(y=>y.name===r),R=String(u?.agentId??"").trim();if(!R)throw new me(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const S=await A.setAgentRelayCredential(r,{agentId:R,virtualKey:a.apiKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:d??a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}});return{restarted:S.restarted===!0,busy:S.busy===!0,...S.pending?{pending:S.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:gt()}});const K=new Ze(v.join(s.dataDir,"remote-admin.json"));if(o.setRemoteAdminGate(()=>K.isEnabled()),await o.start(ht),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=de(p.clientType);A.length!==0&&await b.setAgentRelayEnabled(n,!0,{relayHosts:A}).catch(r=>{i.warn("main",`failed to reconcile relay hosts for "${n}": ${r}`)})}}ue(b,$,_);const ee=parseInt(E["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(!L)throw new he(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return L.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(!L)throw new he(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return L.setAgentRelay(e,!1)},enableHermesProfile:(e,n)=>ct({agentId:e,baseUrl:n.openaiBaseUrl??"",virtualKey:n.virtualKey,model:n.model??""}),disableHermesProfile:e=>lt(e)};x.setRelayCredentialHandler({listHermesProfiles:()=>dt(),enable:async(e,n)=>{const{target:p,result:A}=await ut(te,e,n);return{target:p,...A}},disable:async e=>{const{target:n,result:p}=await pt(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 Se(e)}const Ee=v.join(s.dataDir,"admin-token"),Ae=v.join(s.dataDir,"admin-port");rt(Ee,we),ae(Ae,String(ee),"utf-8"),await G(s.daemonStatusFile,{state:"running",pid:process.pid,instance_id:T,updated_at:Date.now()}),process.send&&process.send("ready"),i.info("main","grix-connector ready")}wt().catch(async t=>{const c=`Fatal: ${t instanceof Error?t.stack??t.message:String(t)}`;try{process.stderr.write(`${c}
|
|
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
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{openSync as
|
|
2
|
-
`;for(let d=0;d<=n;d+=1)try{const
|
|
1
|
+
import{openSync as x,closeSync as D,existsSync as y,readFileSync as s,unlinkSync as h,writeSync as E}from"node:fs";import{mkdir as L}from"node:fs/promises";import{randomUUID as _}from"node:crypto";import f from"node:path";import c from"node:process";import{getProcessCommandLine as m,isProcessRunning as l}from"../service/process-control.js";function w(r){try{const e=JSON.parse(r);if(typeof e?.pid=="number"&&typeof e.acquired_at=="number")return e}catch{}return null}function p(r){if(!y(r))return null;try{return w(s(r,"utf8"))}catch{return null}}function M(r){const e=p(r);return e!==null&&l(e.pid)}class k extends Error{holder;constructor(e){super(`another grix-connector daemon (pid ${e.pid}) is already serving this root; yielding`),this.holder=e,this.name="DaemonAlreadyServingError"}}function O(r,e=2e3,n=2){return async t=>{let u="19579";try{const a=f.join(r,"health-port");y(a)&&(u=s(a,"utf8").trim()||u)}catch{}for(let a=0;a<n;a+=1)try{const d=await fetch(`http://127.0.0.1:${u}/healthz`,{signal:AbortSignal.timeout(e)});if(!d.ok)return!1;const i=await d.json();return i.status==="ok"&&i.pid===t.pid}catch{}return!1}}function S(r){const e=new Int32Array(new SharedArrayBuffer(4));Atomics.wait(e,0,0,r)}function g(r){const e=typeof r.command_line=="string"&&r.command_line?r.command_line:null;if(e===null)return!0;const n=m(r.pid);return n===null?!0:n===e}function j(r){const e=typeof r.command_line=="string"&&r.command_line?r.command_line:null;if(e===null)return!1;const n=m(r.pid);return n!==null&&n===e}function z(r,e={}){const n=e.selfPid??c.pid,t=(e.readLock??p)(r);return!t||t.pid===n||!(e.isRunning??l)(t.pid)?null:(e.isSameProcess??g)(t)?t:null}function v(r){try{return c.kill(r,0),!0}catch(e){return e?.code!=="EPERM"}}function I(r){if(r<=0||r===c.pid)return!0;try{c.kill(r,"SIGTERM")}catch(t){return t?.code!=="EPERM"}const e=Date.now()+3e3;for(;Date.now()<e;){if(!l(r))return!0;S(100)}try{c.kill(r,"SIGKILL")}catch{}const n=Date.now()+1e3;for(;Date.now()<n;){if(!l(r))return!0;S(50)}return!l(r)}async function G(r,e,n=2,t={}){await L(f.dirname(r),{recursive:!0});const u={pid:c.pid,acquired_at:Date.now(),root_dir:f.resolve(e),instance_id:_(),command_line:m(c.pid)},a=`${JSON.stringify(u,null,2)}
|
|
2
|
+
`;for(let d=0;d<=n;d+=1)try{const i=x(r,"wx");try{E(i,a)}finally{D(i)}return u}catch(i){if(!(i instanceof Error)||i.code!=="EEXIST")throw i;let o=null;try{o=w(s(r,"utf8"))}catch{}if(o&&l(o.pid)){if(o.pid===c.pid)throw new Error(`daemon already running (pid ${o.pid}, acquired at ${new Date(o.acquired_at).toISOString()})`);if(v(o.pid)&&g(o)){if(t.isHolderServing&&await t.isHolderServing(o))throw new k(o);if(!I(o.pid))throw new Error(`\u65E0\u6CD5\u7EC8\u6B62\u8001\u8FDB\u7A0B (pid ${o.pid})\uFF0C\u8BF7\u624B\u52A8\u5904\u7406`)}}try{h(r)}catch{}}throw new Error("failed to acquire daemon lock after retries")}async function J(r){const e=p(r);if(e&&e.pid===c.pid)try{h(r)}catch{}}export{k as DaemonAlreadyServingError,G as acquireDaemonLock,O as createHealthzHolderProbe,z as findLiveLockHolder,M as isDaemonLocked,j as isLockHolderIdentityVerified,g as isLockHolderSameProcess,p as readDaemonLock,J as releaseDaemonLock};
|