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.
Files changed (48) hide show
  1. package/dist/adapter/claude/claude-adapter.js +13 -13
  2. package/dist/adapter/claude/usage-parser.js +8 -7
  3. package/dist/assets/dsh-bridge/grix-dsh-bridge-4.9.2-beta.2.tgz +0 -0
  4. package/dist/assets/dsh-bridge/manifest.json +6 -6
  5. package/dist/grix.js +8 -8
  6. package/dist/launcher.mjs +103 -1
  7. package/dist/runtime/daemon-lock.js +2 -2
  8. package/dist/service/platform-adapter.js +30 -30
  9. package/dist/service/process-control.js +1 -1
  10. package/dist/service/service-manager.js +3 -3
  11. package/openclaw-plugin/index.js +1 -1
  12. package/package.json +1 -1
  13. package/scripts/launcher.mjs +103 -1
  14. package/dist/adapter/claude/claude-bridge-server.js +0 -1
  15. package/dist/adapter/claude/claude-tools.js +0 -1
  16. package/dist/adapter/claude/claude-worker-client.js +0 -1
  17. package/dist/adapter/claude/mcp-http-launcher.js +0 -2
  18. package/dist/adapter/claude/result-timeout.js +0 -1
  19. package/dist/adapter/deepseek/deepseek-adapter.js +0 -6
  20. package/dist/adapter/deepseek/index.js +0 -1
  21. package/dist/adapter/qwen/index.js +0 -1
  22. package/dist/adapter/qwen/qwen-adapter.js +0 -4
  23. package/dist/aibot/client.js +0 -1
  24. package/dist/aibot/index.js +0 -1
  25. package/dist/aibot/types.js +0 -0
  26. package/dist/assets/dsh-bridge/grix-dsh-bridge-4.9.1.tgz +0 -0
  27. package/dist/core/access/allowlist-gate.js +0 -1
  28. package/dist/core/access/allowlist-store.js +0 -1
  29. package/dist/core/access/index.js +0 -1
  30. package/dist/core/file-ops/handler.js +0 -1
  31. package/dist/core/file-ops/list-files.js +0 -1
  32. package/dist/core/file-ops/types.js +0 -0
  33. package/dist/log.js +0 -3
  34. package/dist/main.js +0 -31
  35. package/dist/mcp/stream-http/config.js +0 -1
  36. package/dist/mcp/stream-http/connection-binding.js +0 -1
  37. package/dist/mcp/stream-http/event-tool-executor.js +0 -1
  38. package/dist/mcp/stream-http/gateway.js +0 -1
  39. package/dist/mcp/stream-http/index.js +0 -1
  40. package/dist/mcp/stream-http/security.js +0 -1
  41. package/dist/mcp/stream-http/session-manager.js +0 -1
  42. package/dist/mcp/stream-http/tool-executor.js +0 -1
  43. package/dist/mcp/stream-http/tool-registry.js +0 -1
  44. package/dist/mcp/stream-http/tool-schemas.js +0 -1
  45. package/dist/session/index.js +0 -1
  46. package/dist/session/manager.js +0 -1
  47. package/dist/transport/index.js +0 -1
  48. package/dist/transport/json-rpc.js +0 -3
@@ -1,7 +1,8 @@
1
- import{closeSync as O,createReadStream as U,existsSync as M,openSync as F,readdirSync as I,readFileSync as R,readSync as v,realpathSync as X,statSync as w}from"node:fs";import{createInterface as H}from"node:readline";import b from"node:path";import C from"node:os";import{log as E}from"../../core/log/index.js";function q(t){let n=t;try{n=X(t)}catch{}return n.replace(/[^a-zA-Z0-9]/g,"-")}function Q(t,n){const e=b.join(C.homedir(),".claude"),o=b.join(e,"projects",q(n));return b.join(o,`${t}.jsonl`)}const V=5e3,D=64,d=new Map;let j=0;const m=new Map;function Y(t,n){for(d.delete(t),d.set(t,n);d.size>D;){const e=d.keys().next();if(e.done)break;d.delete(e.value)}}function ot(t){t===void 0?(d.clear(),m.clear(),j=0):(d.delete(t),m.delete(t))}function rt(t,n){if(!(!t||!n)&&b.isAbsolute(n)&&b.basename(n)===`${t}.jsonl`&&m.get(t)!==n)for(m.delete(t),m.set(t,n);m.size>D;){const e=m.keys().next();if(e.done)break;m.delete(e.value)}}function Z(t){const n=b.join(C.homedir(),".claude","projects"),e=`${t}.jsonl`;let o;try{o=I(n,{withFileTypes:!0})}catch{return null}let s=null;for(const r of o){if(!r.isDirectory())continue;const c=b.join(n,r.name,e);try{const i=w(c);if(!i.isFile()||i.size===0)continue;(!s||i.size>s.size)&&(s={path:c,size:i.size})}catch{}}return s?.path??null}function k(t,n){const e=m.get(t);if(e&&M(e))return e;const o=Q(t,n);if(M(o))return d.delete(t),o;const s=d.get(t);if(s&&M(s))return s;const r=Date.now();if(r-j<V)return o;j=r;const c=Z(t);return Y(t,c),c?(E.warn("usage-parser",`Session ${t} transcript drifted off the bound cwd: reading ${c} instead of ${o}`),c):o}function J(){return{inputTokens:0,outputTokens:0,cacheReadInputTokens:0,cacheCreationInputTokens:0}}function P(t,n){t.inputTokens+=n.input_tokens??0,t.outputTokens+=n.output_tokens??0,t.cacheReadInputTokens+=n.cache_read_input_tokens??0,t.cacheCreationInputTokens+=n.cache_creation_input_tokens??0}async function it(t,n){const e=k(t,n);E.info("usage-parser",`Parsing session usage from ${e}`);const o=new Map,s=J();let r=0;try{const i=H({input:U(e,"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++,P(s,a);let p=o.get(l);p||(p={turns:0,usage:J()},o.set(l,p)),p.turns++,P(p.usage,a)}catch{}}catch(i){return i.code==="ENOENT"?(E.info("usage-parser",`Session JSONL not found: ${e}`),null):(E.error("usage-parser",`Failed to parse session usage: ${i}`),null)}return r===0?null:{models:[...o.entries()].map(([i,u])=>({model:i,turns:u.turns,total:u.usage})),total:s,turns:r}}const T=64*1024,G=9e4,K=new Set(["No response requested."]);function z(t){return K.has(t.trim())}function L(t){return t.isApiErrorMessage===!0}function ct(t,n,e){const o=k(t,n);try{const s=R(o);let r;e!==void 0&&e>0?r=e<s.length?s.subarray(e):Buffer.alloc(0):r=s.length>T?s.subarray(s.length-T):s;const c=r.toString("utf8").split(`
2
- `);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(L(a))return{text:"",stopReason:i!==void 0?i:l?.stop_reason??null,apiError:!0};if(!l?.content)continue;const p=Array.isArray(l.content)?l.content:[{type:"text",text:String(l.content)}],y=[];let g=0;for(const x of p){const h=x;if(!(h.type!=="text"||!h.text?.trim())){if(z(h.text)){g++;continue}y.push(h.text)}}if(y.length>0)return{text:y.join(`
3
- `),stopReason:i!==void 0?i:l.stop_reason??null};g>0&&i===void 0&&(i=l.stop_reason??null)}return i!==void 0?{text:"",stopReason:i,metaOnly:!0}:null}catch{return null}}function at(t,n,e){const o=k(t,n),s=e>0?e:0;let r;try{const c=w(o).size;if(s>=c)return{segments:[],newOffset:s};const i=c-s,u=Buffer.alloc(i);r=F(o,"r");const f=v(r,u,0,i,s),a=u.subarray(0,f);let l=-1;for(let h=a.length-1;h>=0;h--)if(a[h]===10){l=h;break}if(l<0)return{segments:[],newOffset:s};const p=a.subarray(0,l+1),y=s+p.length,g=[];let x=!1;for(const h of p.toString("utf8").split(`
4
- `)){const N=h.trim();if(!N)continue;let S;try{S=JSON.parse(N)}catch{continue}if(S.type!=="assistant")continue;if(L(S)){x=!0;continue}const _=S.message;if(!_?.content)continue;const $=Array.isArray(_.content)?_.content:[{type:"text",text:String(_.content)}];for(const B of $){const A=B;A.type!=="text"||!A.text?.trim()||z(A.text)||g.push(A.text)}}return{segments:g,newOffset:y,apiError:x}}catch{return{segments:[],newOffset:s}}finally{if(r!==void 0)try{O(r)}catch{}}}function lt(t,n,e){const o=k(t,n);let s=null,r;try{const u=w(o);s=Date.now()-u.mtimeMs,r=R(o)}catch{return{lastStopReason:null,freshMs:null}}let c;e!==void 0&&e>0?c=e<r.length?r.subarray(e):Buffer.alloc(0):c=r.length>T?r.subarray(r.length-T):r;const i=c.toString("utf8").split(`
5
- `);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:s}}return{lastStopReason:null,freshMs:s}}function ut(t,n){const e=k(t,n);let o;try{o=R(e)}catch{return 0}const r=(o.length>T?o.subarray(o.length-T):o).toString("utf8").split(`
6
- `),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 g=u.exec(l);g&&i.add(g[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 ft(t,n,e,o=!1){const s=k(t,n);try{const r=w(s);if(o&&Date.now()-r.mtimeMs<G)return!1;const c=R(s),u=(e>0&&e<c.length?c.subarray(e):c.length>T?c.subarray(c.length-T):c).toString("utf8").split(`
7
- `);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 x=y.message?.content??y.content;(Array.isArray(x)?x:[]).some(S=>typeof S=="object"&&S!==null&&S.type==="tool_result")&&(f=!0,a=!1)}else y.type==="assistant"&&f&&(a=!0)}return f&&!a}catch{return!1}}export{ot as clearSessionJsonlDriftCache,ut as countPendingBackgroundTasks,q as encodeProjectPath,ct as extractLastAssistantEntry,at as extractTextBlocksAfterOffset,ft as hasTerminalToolResultAfterOffset,rt as noteSessionJsonlPath,it as parseClaudeSessionUsage,lt as probeSessionTurnState,Q as resolveBoundSessionJsonlPath,k as resolveSessionJsonlPath};
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};
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bridgeVersion": "4.9.1",
4
- "tarball": "grix-dsh-bridge-4.9.1.tgz",
5
- "size": 23132,
6
- "unpackedSize": 92698,
7
- "shasum": "97579c42a23a1d2fcdcdccf473e33239409202e8",
8
- "integrity": "sha512-qyX9J3iDELSbnnvkMXNDYkExFrRl7R2S2A0UtK7Yd4B7X3Tna0+T/xMZWlNiy+52h6NgNnqYnp3chpdy8aJm8Q=="
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 v from"node:path";import{writeFileSync as ae}from"node:fs";import{Manager as Ie}from"./manager.js";import{ensureGrixDirs as Ce,initLogger as _e,log as i,installProcessLogRotation as ke,setConsoleOutput as ie}from"./core/log/index.js";import{HealthServer as Te,bindPortOrFail as se}from"./core/runtime/index.js";import{writePidFile as $e,removePidFile as H,readDaemonPid as Ue}from"./core/runtime/index.js";import{killOrphanedClaudeProcesses as Le}from"./core/runtime/index.js";import{resolveRuntimePaths as X}from"./core/config/index.js";import{supportsNonMitmRelayConfig as Oe}from"./core/config/provider-env.js";import{ServiceManager as ce}from"./service/service-manager.js";import{RESTART_EXIT_CODE as He,UNHEALTHY_EXIT_CODE as Fe,markManagedStop as le,resolveLauncherStore as V,writeCrashNote as Me}from"./core/upgrade/version-store.js";import{migrateWindowsWrapper as q}from"./service/windows-wrapper-migration.js";const Ne=300*1e3;import{killProcessesByCommandLine as Ge,isWindowsElevated as Be,shouldSweepWindowsWrappers as je,getProcessCommandLine as We}from"./service/process-control.js";import{buildServiceID as Ke}from"./service/service-paths.js";import{acquireDaemonLock as Xe,createHealthzHolderProbe as Ve,DaemonAlreadyServingError as qe,isLockHolderSameProcess as Ye,readDaemonLock as Y,releaseDaemonLock as F}from"./runtime/daemon-lock.js";import{writeDaemonStatus as G,removeDaemonStatus as Je}from"./runtime/service-state.js";import{readDuplicateDaemonRecord as ze,recordDuplicateDaemon as Qe}from"./runtime/duplicate-daemon.js";import{RemoteAdminSettingsStore as Ze}from"./core/config/remote-admin-settings.js";import{AdminServer as et,generateToken as tt,writeTokenFile as rt}from"./core/admin/index.js";import{initSentry as nt,closeSentry as J,reportFatal as z}from"./core/observability/sentry.js";import{initProxyManager as ot,getProxyManager as f,relayHostsForClientType as de,RelayStateStore as at,SecretStore as it}from"./core/proxy/index.js";import{stopProxyIfNobodyNeedsIt as pe,switchAgentRelayMode as Q,reconcileRelayStates as ue}from"./core/proxy/relay-orchestration.js";import{setAgentRelayCredential as ge,RelayCredentialError as me}from"./core/proxy/relay-credential.js";import{RelayFetchError as fe,RelayToggleCoordinator as st}from"./core/proxy/relay-credential-fetch.js";import{applyHermesProfileRelay as ct,clearHermesProfileRelay as lt,discoverHermesProfiles as dt}from"./core/proxy/hermes-profile-relay.js";import{disableRelayCredential as pt,enableRelayCredential as ut,RelayRouteError as he}from"./core/proxy/relay-credential-router.js";import{upgradeStatusSnapshot as gt}from"./core/upgrade/npm-upgrader.js";import{resolveClientVersion as mt}from"./core/util/client-version.js";import{parseCliArgs as ft}from"./core/util/cli-args.js";const{command:I,flags:E,unknownFlags:Z}=ft(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)),E.version&&(console.log(mt()),process.exit(0)),E.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
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=Ue();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 c=Y(X().daemonLockFile);c&&c.pid===t&&!Ye(c)&&(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(c){console.error(`reload failed: ${c instanceof Error?c.message:c}`),process.exit(1)}}const ye=["start","stop","restart","status"];if(I&&ye.includes(I)){process.platform==="win32"&&["start","stop","restart"].includes(I)&&!Be()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
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=X(),c=E["config-dir"]??(E.profile?v.join(t.configDir,E.profile):void 0),m=v.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),g=new ce({cliPath:m,nodePath:process.execPath});try{let l;switch(I){case"start":{const h=await g.healLegacyState({rootDir:t.rootDir});(await g.status({rootDir:t.rootDir})).installed?l=await g.start({rootDir:t.rootDir}):l=await g.install({rootDir:t.rootDir,configDir:c??h.configDir}),h.healed&&(l={...l,legacy_heal:h.reasons})}break;case"stop":l=await g.stop({rootDir:t.rootDir});break;case"restart":(await g.status({rootDir:t.rootDir})).installed?l=await g.restart({rootDir:t.rootDir}):l=await g.install({rootDir:t.rootDir,configDir:c});break;case"status":l=await g.status({rootDir:t.rootDir});{const h=ze(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: ${ye.join(", ")}`),process.exit(1));const s=X(),ht=E["config-dir"]??(E.profile?`${s.configDir}/${E.profile}`:void 0),o=new Ie({requestGracefulShutdown:t=>{C(t)},onTransactionSettled:()=>{q({rootDir:s.rootDir,requestGracefulShutdown:()=>{C("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 Te,we=tt(),x=new et(we);let B=!1,T;function j(t,c){const m=V(),g=process.env.GRIX_LAUNCHER_VERSION;if(!m||!g)return;const l=c instanceof Error?c.stack??c.message:String(c);Me(m,g,`${t}: ${l}`)}function yt(t){const c=`another grix-connector daemon (pid ${t.pid}) is already serving ${s.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.`;Qe(s.dataDir,{holder_pid:t.pid,holder_command_line:t.command_line,yielded_pid:process.pid,yielded_command_line:We(process.pid)}),process.stderr.write(`${c}
41
- `),i.warn("main",c);const m=V();m&&le(m),process.exit(0)}async function Se(t){process.stderr.write(t.message+`
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",c),z(t,"startup"),j("startup",t);const m=Y(s.daemonLockFile);m?.pid===process.pid&&m.instance_id===T&&await G(s.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:m.instance_id,updated_at:Date.now(),reason:"startup_error"}).catch(()=>{}),await J(),F(s.daemonLockFile).catch(()=>{}),H(),process.exit(1)});const St=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function Re(t){return t instanceof Error&&"code"in t?St.has(t.code):!1}
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 = 2;
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 g,closeSync as D,existsSync as p,readFileSync as s,unlinkSync as y,writeSync as E}from"node:fs";import{mkdir as x}from"node:fs/promises";import{randomUUID as _}from"node:crypto";import f from"node:path";import i from"node:process";import{getProcessCommandLine as m,isProcessRunning as l}from"../service/process-control.js";function h(r){try{const e=JSON.parse(r);if(typeof e?.pid=="number"&&typeof e.acquired_at=="number")return e}catch{}return null}function w(r){if(!p(r))return null;try{return h(s(r,"utf8"))}catch{return null}}function O(r){const e=w(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 j(r,e=2e3,n=2){return async c=>{let u="19579";try{const a=f.join(r,"health-port");p(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 o=await d.json();return o.status==="ok"&&o.pid===c.pid}catch{}return!1}}function S(r){const e=new Int32Array(new SharedArrayBuffer(4));Atomics.wait(e,0,0,r)}function L(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 z(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 I(r){try{return i.kill(r,0),!0}catch(e){return e?.code!=="EPERM"}}function v(r){if(r<=0||r===i.pid)return!0;try{i.kill(r,"SIGTERM")}catch(c){return c?.code!=="EPERM"}const e=Date.now()+3e3;for(;Date.now()<e;){if(!l(r))return!0;S(100)}try{i.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,c={}){await x(f.dirname(r),{recursive:!0});const u={pid:i.pid,acquired_at:Date.now(),root_dir:f.resolve(e),instance_id:_(),command_line:m(i.pid)},a=`${JSON.stringify(u,null,2)}
2
- `;for(let d=0;d<=n;d+=1)try{const o=g(r,"wx");try{E(o,a)}finally{D(o)}return u}catch(o){if(!(o instanceof Error)||o.code!=="EEXIST")throw o;let t=null;try{t=h(s(r,"utf8"))}catch{}if(t&&l(t.pid)){if(t.pid===i.pid)throw new Error(`daemon already running (pid ${t.pid}, acquired at ${new Date(t.acquired_at).toISOString()})`);if(I(t.pid)&&L(t)){if(c.isHolderServing&&await c.isHolderServing(t))throw new k(t);if(!v(t.pid))throw new Error(`\u65E0\u6CD5\u7EC8\u6B62\u8001\u8FDB\u7A0B (pid ${t.pid})\uFF0C\u8BF7\u624B\u52A8\u5904\u7406`)}}try{y(r)}catch{}}throw new Error("failed to acquire daemon lock after retries")}async function J(r){const e=w(r);if(e&&e.pid===i.pid)try{y(r)}catch{}}export{k as DaemonAlreadyServingError,G as acquireDaemonLock,j as createHealthzHolderProbe,O as isDaemonLocked,z as isLockHolderIdentityVerified,L as isLockHolderSameProcess,w as readDaemonLock,J as releaseDaemonLock};
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};