@hellcoder/companion 0.104.3 → 0.104.4-preview.20260622030455.280fa4c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentsPage-H1WOCJ_U.js → AgentsPage-C_uaskvu.js} +1 -1
- package/dist/assets/{CronManager-Cqnqj1KO.js → CronManager-_ScZYZUL.js} +1 -1
- package/dist/assets/{IntegrationsPage-Nd6In9Av.js → IntegrationsPage-j39LFDWj.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-D7RQfyFI.js → LinearOAuthSettingsPage-B437XSi7.js} +1 -1
- package/dist/assets/{LinearSettingsPage-CGVs5jG6.js → LinearSettingsPage-CrHvZJ2l.js} +1 -1
- package/dist/assets/{Playground-M3TI9s_p.js → Playground-Vo-LT0XD.js} +1 -1
- package/dist/assets/{PromptsPage-QXhg1mXT.js → PromptsPage-CBFfssun.js} +1 -1
- package/dist/assets/{RunsPage-C1XiccgQ.js → RunsPage-CzkUlLNh.js} +1 -1
- package/dist/assets/{SandboxManager-DYmKXcTM.js → SandboxManager-F4OKDTsR.js} +1 -1
- package/dist/assets/{SettingsPage-DaJfAHDI.js → SettingsPage-BMOYx-nn.js} +1 -1
- package/dist/assets/{TailscalePage-CmH09EMI.js → TailscalePage-Cs98ENqe.js} +1 -1
- package/dist/assets/index-BkJgPfVV.js +136 -0
- package/dist/assets/{sw-register-DkUWP5b0.js → sw-register-CNrT7Vew.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/claude-adapter.ts +18 -8
- package/server/cli-launcher.test.ts +92 -0
- package/server/cli-launcher.ts +54 -1
- package/server/event-bus-types.ts +10 -2
- package/server/session-orchestrator.test.ts +65 -0
- package/server/session-orchestrator.ts +40 -1
- package/server/session-types.test.ts +86 -0
- package/server/session-types.ts +57 -0
- package/server/ws-bridge-types.ts +20 -0
- package/server/ws-bridge.test.ts +178 -0
- package/server/ws-bridge.ts +59 -0
- package/dist/assets/index-CKx0s4-A.js +0 -134
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-BkJgPfVV.js";function w(r={}){const{immediate:i=!1,onNeedReload:d,onNeedRefresh:_,onOfflineReady:s,onRegistered:n,onRegisteredSW:o,onRegisterError:t}=r;let a,c;const l=async(e=!0)=>{await c};async function f(){if("serviceWorker"in navigator){if(a=await u(async()=>{const{Workbox:e}=await import("./workbox-window.prod.es5-BBnX5xw4.js");return{Workbox:e}},[]).then(({Workbox:e})=>new e("/sw.js",{scope:"/",type:"classic"})).catch(e=>{t==null||t(e)}),!a)return;a.addEventListener("activated",e=>{(e.isUpdate||e.isExternal)&&(d?d():window.location.reload())}),a.addEventListener("installed",e=>{e.isUpdate||s==null||s()}),a.register({immediate:i}).then(e=>{o?o("/sw.js",e):n==null||n(e)}).catch(e=>{t==null||t(e)})}}return c=f(),l}w({onRegisteredSW(r,i){i&&setInterval(()=>{i.update()},3600*1e3)},onOfflineReady(){console.log("[SW] Offline-ready: all assets precached")}});
|
package/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="Companion ME" />
|
|
12
12
|
<meta name="theme-color" content="#d97757" />
|
|
13
13
|
<title>The Companion — Moritz Edition</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-BkJgPfVV.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-psWeuMrk.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-cc-bg text-cc-fg">
|
package/dist/sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
try{self["workbox:core:7.4.0"]&&_()}catch{}const x=(a,...e)=>{let t=a;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},N=x;class l extends Error{constructor(e,t){const s=N(e,t);super(s),this.name=e,this.details=t}}const E=new Set,f={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},b=a=>[f.prefix,a,f.suffix].filter(e=>e&&e.length>0).join("-"),O=a=>{for(const e of Object.keys(f))a(e)},C={updateDetails:a=>{O(e=>{typeof a[e]=="string"&&(f[e]=a[e])})},getGoogleAnalyticsName:a=>a||b(f.googleAnalytics),getPrecacheName:a=>a||b(f.precache),getPrefix:()=>f.prefix,getRuntimeName:a=>a||b(f.runtime),getSuffix:()=>f.suffix};function P(a,e){const t=new URL(a);for(const s of e)t.searchParams.delete(s);return t.href}async function I(a,e,t,s){const n=P(e.url,t);if(e.url===n)return a.match(e,s);const r=Object.assign(Object.assign({},s),{ignoreSearch:!0}),c=await a.keys(e,r);for(const i of c){const o=P(i.url,t);if(n===o)return a.match(i,s)}}let y;function M(){if(y===void 0){const a=new Response("");if("body"in a)try{new Response(a.body),y=!0}catch{y=!1}y=!1}return y}class D{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}async function S(){for(const a of E)await a()}const W=a=>new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),"");function A(a){return new Promise(e=>setTimeout(e,a))}function K(a,e){const t=e();return a.waitUntil(t),t}async function q(a,e){let t=null;if(a.url&&(t=new URL(a.url).origin),t!==self.location.origin)throw new l("cross-origin-copy-response",{origin:t});const s=a.clone(),r={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},c=M()?s.body:await s.blob();return new Response(c,r)}function j(){self.addEventListener("activate",()=>self.clients.claim())}try{self["workbox:precaching:7.4.0"]&&_()}catch{}const H="__WB_REVISION__";function F(a){if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(typeof a=="string"){const r=new URL(a,location.href);return{cacheKey:r.href,url:r.href}}const{revision:e,url:t}=a;if(!t)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(!e){const r=new URL(t,location.href);return{cacheKey:r.href,url:r.href}}const s=new URL(t,location.href),n=new URL(t,location.href);return s.searchParams.set(H,e),{cacheKey:s.href,url:n.href}}class B{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if(e.type==="install"&&t&&t.originalRequest&&t.originalRequest instanceof Request){const n=t.originalRequest.url;s?this.notUpdatedURLs.push(n):this.updatedURLs.push(n)}return s}}}class ${constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const n=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return n?new Request(n,{headers:t.headers}):t},this._precacheController=e}}try{self["workbox:strategies:7.4.0"]&&_()}catch{}function m(a){return typeof a=="string"?new Request(a):a}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new D,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:t}=this;let s=m(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const c=await t.preloadResponse;if(c)return c}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const c of this.iterateCallbacks("requestWillFetch"))s=await c({request:s.clone(),event:t})}catch(c){if(c instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:c.message})}const r=s.clone();try{let c;c=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))c=await i({event:t,request:r,response:c});return c}catch(c){throw n&&await this.runCallbacks("fetchDidFail",{error:c,event:t,originalRequest:n.clone(),request:r.clone()}),c}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=m(e);let s;const{cacheName:n,matchOptions:r}=this._strategy,c=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(c,i);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:n,matchOptions:r,cachedResponse:s,request:c,event:this.event})||void 0;return s}async cachePut(e,t){const s=m(e);await A(0);const n=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:W(n.url)});const r=await this._ensureResponseSafeToCache(t);if(!r)return!1;const{cacheName:c,matchOptions:i}=this._strategy,o=await self.caches.open(c),h=this.hasCallback("cacheDidUpdate"),p=h?await I(o,n.clone(),["__WB_REVISION__"],i):null;try{await o.put(n,h?r.clone():r)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await S(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:c,oldResponse:p,newResponse:r.clone(),request:n,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const r of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await r({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield r=>{const c=Object.assign(Object.assign({},r),{state:s});return t[e](c)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){const e=this._extendLifetimePromises.splice(0),s=(await Promise.allSettled(e)).find(n=>n.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=await n({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t}}class V{constructor(e={}){this.cacheName=C.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,n="params"in e?e.params:void 0,r=new G(this,{event:t,request:s,params:n}),c=this._getResponse(r,s,t),i=this._awaitComplete(c,r,s,t);return[c,i]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=await this._handle(t,e),!n||n.type==="error")throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(const c of e.iterateCallbacks("handlerDidError"))if(n=await c({error:r,event:s,request:t}),n)break}if(!n)throw r}for(const r of e.iterateCallbacks("handlerWillRespond"))n=await r({event:s,request:t,response:n});return n}async _awaitComplete(e,t,s,n){let r,c;try{r=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await t.doneWaiting()}catch(i){i instanceof Error&&(c=i)}if(await t.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:c}),t.destroy(),c)throw c}}class d extends V{constructor(e={}){e.cacheName=C.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(d.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&t.event.type==="install"?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s;const n=t.params||{};if(this._fallbackToNetwork){const r=n.integrity,c=e.integrity,i=!c||c===r;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?c||r:void 0})),r&&i&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new l("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();const s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,n]of this.plugins.entries())n!==d.copyRedirectedCacheableResponsesPlugin&&(n===d.defaultPrecacheCacheabilityPlugin&&(e=s),n.cacheWillUpdate&&t++);t===0?this.plugins.push(d.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}d.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:a}){return!a||a.status>=400?null:a}};d.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:a}){return a.redirected?await q(a):a}};class Q{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new d({cacheName:C.getPrecacheName(e),plugins:[...t,new $({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const t=[];for(const s of e){typeof s=="string"?t.push(s):s&&s.revision===void 0&&t.push(s.url);const{cacheKey:n,url:r}=F(s),c=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(r)&&this._urlsToCacheKeys.get(r)!==n)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(r),secondEntry:n});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(n)&&this._cacheKeysToIntegrities.get(n)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:r});this._cacheKeysToIntegrities.set(n,s.integrity)}if(this._urlsToCacheKeys.set(r,n),this._urlsToCacheModes.set(r,c),t.length>0){const i=`Workbox is precaching URLs without revision info: ${t.join(", ")}
|
|
2
|
-
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"
|
|
2
|
+
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"9aa416b1d5959317912335f858d9ee7b","url":"index.html"},{"revision":"1421e9c6cad8218ef5689b85034d3a95","url":"icon-512.png"},{"revision":"b96da5a0b6636012517f54b0e69c304d","url":"icon-192.png"},{"revision":"c64ca8ae8596d4ffe13d0f591beb2274","url":"favicon.svg"},{"revision":"6481face04bd08e6f0bfc2a633840b57","url":"apple-touch-icon.png"},{"revision":"83304194a3c8be2b5b61242eeb1c1046","url":"fonts/MesloLGSNerdFontMono-Regular.woff2"},{"revision":"60fafe18cbcb717c51cdabf87f9490f0","url":"fonts/MesloLGSNerdFontMono-Bold.woff2"},{"revision":null,"url":"assets/workbox-window.prod.es5-BBnX5xw4.js"},{"revision":null,"url":"assets/sw-register-CNrT7Vew.js"},{"revision":null,"url":"assets/index-psWeuMrk.css"},{"revision":null,"url":"assets/index-BkJgPfVV.js"},{"revision":null,"url":"assets/TailscalePage-Cs98ENqe.js"},{"revision":null,"url":"assets/SettingsPage-BMOYx-nn.js"},{"revision":null,"url":"assets/SandboxManager-F4OKDTsR.js"},{"revision":null,"url":"assets/RunsPage-CzkUlLNh.js"},{"revision":null,"url":"assets/PromptsPage-CBFfssun.js"},{"revision":null,"url":"assets/Playground-Vo-LT0XD.js"},{"revision":null,"url":"assets/LinearSettingsPage-CrHvZJ2l.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-B437XSi7.js"},{"revision":null,"url":"assets/IntegrationsPage-j39LFDWj.js"},{"revision":null,"url":"assets/CronManager-_ScZYZUL.js"},{"revision":null,"url":"assets/AgentsPage-C_uaskvu.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hellcoder/companion",
|
|
3
|
-
"version": "0.104.
|
|
3
|
+
"version": "0.104.4-preview.20260622030455.280fa4c",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web UI for launching and interacting with Claude Code agents — Moritz Edition (fork of the-companion)",
|
|
6
6
|
"license": "MIT",
|
package/server/claude-adapter.ts
CHANGED
|
@@ -560,8 +560,9 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
560
560
|
parent_tool_use_id: null,
|
|
561
561
|
session_id: msg.session_id || "",
|
|
562
562
|
});
|
|
563
|
-
|
|
564
|
-
|
|
563
|
+
// Propagate delivery success so the bridge re-queues an undelivered prompt
|
|
564
|
+
// (write to a dying stdio pipe) rather than treating send() as delivery.
|
|
565
|
+
return this.sendToBackend(ndjson);
|
|
565
566
|
}
|
|
566
567
|
|
|
567
568
|
/**
|
|
@@ -1185,18 +1186,20 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
1185
1186
|
* Send an NDJSON string to the CLI. If the CLI socket is not yet connected,
|
|
1186
1187
|
* queues the message for later delivery (flushed in attachWebSocket).
|
|
1187
1188
|
*/
|
|
1188
|
-
private sendToBackend(ndjson: string):
|
|
1189
|
+
private sendToBackend(ndjson: string): boolean {
|
|
1189
1190
|
if (!this.isConnected()) {
|
|
1190
1191
|
console.log(
|
|
1191
1192
|
`[claude-adapter] CLI not yet connected for session ${this.sessionId}, queuing message`,
|
|
1192
1193
|
);
|
|
1193
1194
|
this.pendingMessages.push(ndjson);
|
|
1194
|
-
|
|
1195
|
+
// Safely queued in the adapter's own pre-init buffer — the caller should
|
|
1196
|
+
// NOT also re-queue it (returning true means "handled, don't re-queue").
|
|
1197
|
+
return true;
|
|
1195
1198
|
}
|
|
1196
1199
|
// In stdio mode, send the one-time initialize handshake before the first
|
|
1197
1200
|
// real message (enables the can_use_tool permission flow).
|
|
1198
1201
|
if (this.transportKind === "stdio") this.ensureStdioInitialized(ndjson);
|
|
1199
|
-
this.sendRaw(ndjson);
|
|
1202
|
+
return this.sendRaw(ndjson);
|
|
1200
1203
|
}
|
|
1201
1204
|
|
|
1202
1205
|
/**
|
|
@@ -1204,7 +1207,7 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
1204
1207
|
* delimiter and records the outgoing message. Assumes the transport is
|
|
1205
1208
|
* connected (callers gate on isConnected() / flush after attach).
|
|
1206
1209
|
*/
|
|
1207
|
-
private sendRaw(ndjson: string):
|
|
1210
|
+
private sendRaw(ndjson: string): boolean {
|
|
1208
1211
|
// Record raw outgoing CLI message
|
|
1209
1212
|
this.recorder?.record(
|
|
1210
1213
|
this.sessionId, "out", ndjson, "cli", "claude", "",
|
|
@@ -1212,15 +1215,22 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
1212
1215
|
try {
|
|
1213
1216
|
// NDJSON requires a newline delimiter
|
|
1214
1217
|
if (this.transportKind === "stdio") {
|
|
1215
|
-
|
|
1218
|
+
// A null writer means the transport is gone (process exiting/exited)
|
|
1219
|
+
// even though isConnected() may not have flipped yet. Report failure so
|
|
1220
|
+
// the caller re-queues the message instead of losing it down a dead pipe.
|
|
1221
|
+
if (!this.stdioWriter) return false;
|
|
1222
|
+
this.stdioWriter.write(new TextEncoder().encode(ndjson + "\n"));
|
|
1216
1223
|
} else {
|
|
1217
|
-
this.cliSocket
|
|
1224
|
+
if (!this.cliSocket) return false;
|
|
1225
|
+
this.cliSocket.send(ndjson + "\n");
|
|
1218
1226
|
}
|
|
1227
|
+
return true;
|
|
1219
1228
|
} catch (err) {
|
|
1220
1229
|
console.error(
|
|
1221
1230
|
`[claude-adapter] Failed to send to CLI for session ${this.sessionId}:`,
|
|
1222
1231
|
err,
|
|
1223
1232
|
);
|
|
1233
|
+
return false;
|
|
1224
1234
|
}
|
|
1225
1235
|
}
|
|
1226
1236
|
}
|
|
@@ -256,6 +256,26 @@ describe("launch", () => {
|
|
|
256
256
|
expect(cmdAndArgs[modeIdx + 1]).toBe("bypassPermissions");
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
+
it("passes --effort when an effort level is provided", () => {
|
|
260
|
+
// The reasoning-effort selector sends e.g. "xhigh"; it must reach the CLI
|
|
261
|
+
// as `--effort xhigh`.
|
|
262
|
+
launcher.launch({ effort: "xhigh", cwd: "/tmp" });
|
|
263
|
+
|
|
264
|
+
const [cmdAndArgs] = mockSpawn.mock.calls[0];
|
|
265
|
+
const effortIdx = cmdAndArgs.indexOf("--effort");
|
|
266
|
+
expect(effortIdx).toBeGreaterThan(-1);
|
|
267
|
+
expect(cmdAndArgs[effortIdx + 1]).toBe("xhigh");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("omits --effort when no effort level is provided (model default)", () => {
|
|
271
|
+
// Empty/undefined effort means "use the model's built-in default" — the flag
|
|
272
|
+
// must not appear at all so the CLI doesn't override the default.
|
|
273
|
+
launcher.launch({ cwd: "/tmp" });
|
|
274
|
+
|
|
275
|
+
const [cmdAndArgs] = mockSpawn.mock.calls[0];
|
|
276
|
+
expect(cmdAndArgs).not.toContain("--effort");
|
|
277
|
+
});
|
|
278
|
+
|
|
259
279
|
it("downgrades bypassPermissions to acceptEdits for containerized Claude sessions", () => {
|
|
260
280
|
launcher.launch({
|
|
261
281
|
cwd: "/tmp/project",
|
|
@@ -1385,3 +1405,75 @@ describe("isCmdScript platform guard", () => {
|
|
|
1385
1405
|
expect(cmdAndArgs[0]).not.toBe("cmd.exe");
|
|
1386
1406
|
});
|
|
1387
1407
|
});
|
|
1408
|
+
|
|
1409
|
+
// ─── Exit classification (auth vs crash vs normal) ───────────────────────────
|
|
1410
|
+
// The orchestrator must NOT auto-relaunch a CLI that exited because its
|
|
1411
|
+
// credentials are invalid/expired — relaunching just loops into the same 401.
|
|
1412
|
+
// cli-launcher classifies the exit from a tail of stderr + the exit code and
|
|
1413
|
+
// stamps `reason` onto the session:exited event.
|
|
1414
|
+
describe("exit reason classification", () => {
|
|
1415
|
+
function makeProcWithStderr(stderrText: string) {
|
|
1416
|
+
let resolveExit: (c: number) => void;
|
|
1417
|
+
const exited = new Promise<number>((r) => { resolveExit = r; });
|
|
1418
|
+
const proc = {
|
|
1419
|
+
pid: 4242,
|
|
1420
|
+
kill: vi.fn(),
|
|
1421
|
+
exited,
|
|
1422
|
+
stdin: new WritableStream<Uint8Array>(),
|
|
1423
|
+
stdout: new ReadableStream<Uint8Array>({ start() {} }),
|
|
1424
|
+
stderr: new ReadableStream<Uint8Array>({
|
|
1425
|
+
start(controller) {
|
|
1426
|
+
if (stderrText) controller.enqueue(new TextEncoder().encode(stderrText));
|
|
1427
|
+
controller.close();
|
|
1428
|
+
},
|
|
1429
|
+
}),
|
|
1430
|
+
};
|
|
1431
|
+
return { proc, resolveExit: resolveExit! };
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
it("classifies a 401 stderr exit as reason 'auth'", async () => {
|
|
1435
|
+
const { proc, resolveExit } = makeProcWithStderr(
|
|
1436
|
+
"API Error: 401 Invalid authentication credentials\n",
|
|
1437
|
+
);
|
|
1438
|
+
mockSpawn.mockReturnValueOnce(proc);
|
|
1439
|
+
const events: any[] = [];
|
|
1440
|
+
companionBus.on("session:exited", (e) => { events.push(e); });
|
|
1441
|
+
|
|
1442
|
+
launcher.launch({ cwd: "/tmp" });
|
|
1443
|
+
// Let pipeStream drain the stderr chunk into the tail before the exit fires.
|
|
1444
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1445
|
+
resolveExit(1);
|
|
1446
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1447
|
+
|
|
1448
|
+
expect(events).toHaveLength(1);
|
|
1449
|
+
expect(events[0].reason).toBe("auth");
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it("classifies a clean (code 0) exit with no auth stderr as 'normal'", async () => {
|
|
1453
|
+
const { proc, resolveExit } = makeProcWithStderr("");
|
|
1454
|
+
mockSpawn.mockReturnValueOnce(proc);
|
|
1455
|
+
const events: any[] = [];
|
|
1456
|
+
companionBus.on("session:exited", (e) => { events.push(e); });
|
|
1457
|
+
|
|
1458
|
+
launcher.launch({ cwd: "/tmp" });
|
|
1459
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1460
|
+
resolveExit(0);
|
|
1461
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1462
|
+
|
|
1463
|
+
expect(events[0].reason).toBe("normal");
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
it("classifies a non-zero exit with no auth stderr as 'crash'", async () => {
|
|
1467
|
+
const { proc, resolveExit } = makeProcWithStderr("Segmentation fault\n");
|
|
1468
|
+
mockSpawn.mockReturnValueOnce(proc);
|
|
1469
|
+
const events: any[] = [];
|
|
1470
|
+
companionBus.on("session:exited", (e) => { events.push(e); });
|
|
1471
|
+
|
|
1472
|
+
launcher.launch({ cwd: "/tmp" });
|
|
1473
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1474
|
+
resolveExit(139);
|
|
1475
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1476
|
+
|
|
1477
|
+
expect(events[0].reason).toBe("crash");
|
|
1478
|
+
});
|
|
1479
|
+
});
|
package/server/cli-launcher.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import type { Subprocess } from "bun";
|
|
12
12
|
import type { SessionStore } from "./session-store.js";
|
|
13
13
|
import type { BackendType } from "./session-types.js";
|
|
14
|
+
import { looksLikeAuthErrorText } from "./session-types.js";
|
|
14
15
|
import type { RecorderManager } from "./recorder.js";
|
|
15
16
|
import { CodexAdapter } from "./codex-adapter.js";
|
|
16
17
|
import { ClaudeAdapter } from "./claude-adapter.js";
|
|
@@ -83,6 +84,8 @@ export interface SdkSessionInfo {
|
|
|
83
84
|
exitCode?: number | null;
|
|
84
85
|
model?: string;
|
|
85
86
|
permissionMode?: string;
|
|
87
|
+
/** Reasoning-effort level (low|medium|high|xhigh|max) passed to the CLI's --effort flag. Empty/undefined = model default. Claude only. */
|
|
88
|
+
effort?: string;
|
|
86
89
|
cwd: string;
|
|
87
90
|
createdAt: number;
|
|
88
91
|
/** The CLI's internal session ID (from system.init), used for --resume */
|
|
@@ -148,6 +151,8 @@ export interface SdkSessionInfo {
|
|
|
148
151
|
export interface LaunchOptions {
|
|
149
152
|
model?: string;
|
|
150
153
|
permissionMode?: string;
|
|
154
|
+
/** Reasoning-effort level (low|medium|high|xhigh|max). Empty/undefined = model default. Claude only. */
|
|
155
|
+
effort?: string;
|
|
151
156
|
cwd?: string;
|
|
152
157
|
claudeBinary?: string;
|
|
153
158
|
codexBinary?: string;
|
|
@@ -193,6 +198,12 @@ export class CliLauncher {
|
|
|
193
198
|
private claimedCodexWsPorts = new Set<number>();
|
|
194
199
|
/** Runtime-only env vars per session (kept out of persisted launcher state). */
|
|
195
200
|
private sessionEnvs = new Map<string, Record<string, string>>();
|
|
201
|
+
/**
|
|
202
|
+
* Rolling tail of recent stderr per session, used to classify WHY a process
|
|
203
|
+
* exited (e.g. an auth failure vs a recoverable crash). Capped per session.
|
|
204
|
+
*/
|
|
205
|
+
private stderrTails = new Map<string, string>();
|
|
206
|
+
private static readonly STDERR_TAIL_MAX = 4096;
|
|
196
207
|
private store: SessionStore | null = null;
|
|
197
208
|
private recorder: RecorderManager | null = null;
|
|
198
209
|
|
|
@@ -302,6 +313,7 @@ export class CliLauncher {
|
|
|
302
313
|
state: "starting",
|
|
303
314
|
model: options.model,
|
|
304
315
|
permissionMode: options.permissionMode,
|
|
316
|
+
effort: options.effort,
|
|
305
317
|
cwd,
|
|
306
318
|
createdAt: Date.now(),
|
|
307
319
|
backendType,
|
|
@@ -452,6 +464,7 @@ export class CliLauncher {
|
|
|
452
464
|
this.spawnCLI(sessionId, info, {
|
|
453
465
|
model: info.model,
|
|
454
466
|
permissionMode: info.permissionMode,
|
|
467
|
+
effort: info.effort,
|
|
455
468
|
cwd: info.cwd,
|
|
456
469
|
resumeSessionId: info.cliSessionId,
|
|
457
470
|
containerId: info.containerId,
|
|
@@ -525,6 +538,11 @@ export class CliLauncher {
|
|
|
525
538
|
if (options.model) {
|
|
526
539
|
args.push("--model", options.model);
|
|
527
540
|
}
|
|
541
|
+
if (options.effort) {
|
|
542
|
+
// Reasoning-effort level (low|medium|high|xhigh|max). Omitted when empty so
|
|
543
|
+
// the model falls back to its built-in default effort.
|
|
544
|
+
args.push("--effort", options.effort);
|
|
545
|
+
}
|
|
528
546
|
if (effectivePermissionMode) {
|
|
529
547
|
args.push("--permission-mode", effectivePermissionMode);
|
|
530
548
|
}
|
|
@@ -640,7 +658,9 @@ export class CliLauncher {
|
|
|
640
658
|
}
|
|
641
659
|
this.processes.delete(sessionId);
|
|
642
660
|
this.persistState();
|
|
643
|
-
|
|
661
|
+
const reason = this.classifyExitReason(sessionId, exitCode);
|
|
662
|
+
this.stderrTails.delete(sessionId);
|
|
663
|
+
companionBus.emit("session:exited", { sessionId, exitCode, reason });
|
|
644
664
|
});
|
|
645
665
|
|
|
646
666
|
this.persistState();
|
|
@@ -1269,6 +1289,28 @@ export class CliLauncher {
|
|
|
1269
1289
|
await Promise.all(ids.map((id) => this.kill(id)));
|
|
1270
1290
|
}
|
|
1271
1291
|
|
|
1292
|
+
/**
|
|
1293
|
+
* Classify why a CLI process exited from its exit code and a tail of stderr.
|
|
1294
|
+
* "auth" means expired/invalid credentials — the orchestrator must NOT
|
|
1295
|
+
* auto-relaunch (it would loop straight back into the same failure) and
|
|
1296
|
+
* should surface a re-login affordance instead. "normal" is a clean exit;
|
|
1297
|
+
* everything else is a recoverable "crash".
|
|
1298
|
+
*/
|
|
1299
|
+
private classifyExitReason(
|
|
1300
|
+
sessionId: string,
|
|
1301
|
+
exitCode: number | null,
|
|
1302
|
+
): "auth" | "normal" | "crash" {
|
|
1303
|
+
const stderr = this.stderrTails.get(sessionId) ?? "";
|
|
1304
|
+
// Uses the shared, tightened matcher (see looksLikeAuthErrorText): bare
|
|
1305
|
+
// status numbers in stderr — stack-trace line offsets, tool exit codes —
|
|
1306
|
+
// must NOT be read as auth, or a recoverable crash gets its relaunch
|
|
1307
|
+
// suppressed and the session hangs dead.
|
|
1308
|
+
if (looksLikeAuthErrorText(stderr)) {
|
|
1309
|
+
return "auth";
|
|
1310
|
+
}
|
|
1311
|
+
return exitCode === 0 ? "normal" : "crash";
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1272
1314
|
private async pipeStream(
|
|
1273
1315
|
sessionId: string,
|
|
1274
1316
|
stream: ReadableStream<Uint8Array> | null,
|
|
@@ -1286,6 +1328,17 @@ export class CliLauncher {
|
|
|
1286
1328
|
if (text.trim()) {
|
|
1287
1329
|
log(`[session:${sessionId}:${label}] ${text.trimEnd()}`);
|
|
1288
1330
|
}
|
|
1331
|
+
// Keep a rolling tail of stderr so process-exit can be classified
|
|
1332
|
+
// (auth failure vs recoverable crash) without a second reader.
|
|
1333
|
+
if (label === "stderr" && text) {
|
|
1334
|
+
const merged = (this.stderrTails.get(sessionId) ?? "") + text;
|
|
1335
|
+
this.stderrTails.set(
|
|
1336
|
+
sessionId,
|
|
1337
|
+
merged.length > CliLauncher.STDERR_TAIL_MAX
|
|
1338
|
+
? merged.slice(-CliLauncher.STDERR_TAIL_MAX)
|
|
1339
|
+
: merged,
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1289
1342
|
}
|
|
1290
1343
|
} catch {
|
|
1291
1344
|
// stream closed
|
|
@@ -12,8 +12,16 @@ export interface CompanionEventMap {
|
|
|
12
12
|
/** CLI reported its internal session ID (used for --resume). */
|
|
13
13
|
"session:cli-id-received": { sessionId: string; cliSessionId: string };
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* CLI/Codex process exited. `reason` classifies why, derived from the exit
|
|
17
|
+
* code and a tail of stderr: "auth" (expired/invalid credentials — must NOT
|
|
18
|
+
* auto-relaunch), "normal" (clean exit), or "crash"/undefined (recoverable).
|
|
19
|
+
*/
|
|
20
|
+
"session:exited": {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
exitCode: number | null;
|
|
23
|
+
reason?: "auth" | "normal" | "crash";
|
|
24
|
+
};
|
|
17
25
|
|
|
18
26
|
/** CLI WebSocket disconnected and a browser needs a relaunch. */
|
|
19
27
|
"session:relaunch-needed": { sessionId: string };
|
|
@@ -1931,4 +1931,69 @@ describe("SessionOrchestrator", () => {
|
|
|
1931
1931
|
expect(deps.launcher.relaunch).not.toHaveBeenCalled();
|
|
1932
1932
|
});
|
|
1933
1933
|
});
|
|
1934
|
+
|
|
1935
|
+
// ── Auth-failure exits: never auto-relaunch, surface re-login ──────────────
|
|
1936
|
+
describe("auth-failure exits", () => {
|
|
1937
|
+
it("skips auto-relaunch and broadcasts auth_status on an auth-classified exit", () => {
|
|
1938
|
+
vi.useFakeTimers();
|
|
1939
|
+
orchestrator.initialize();
|
|
1940
|
+
// A session that would otherwise be a relaunch candidate.
|
|
1941
|
+
deps.launcher.getSession.mockReturnValue({ archived: false, state: "exited", pid: undefined } as any);
|
|
1942
|
+
deps.wsBridge.isCliConnected.mockReturnValue(false);
|
|
1943
|
+
|
|
1944
|
+
companionBus.emit("session:exited", { sessionId: "s1", exitCode: 1, reason: "auth" });
|
|
1945
|
+
// Even after any keepalive backoff window, no relaunch must occur.
|
|
1946
|
+
vi.advanceTimersByTime(60_000);
|
|
1947
|
+
|
|
1948
|
+
expect(deps.wsBridge.broadcastToSession).toHaveBeenCalledWith(
|
|
1949
|
+
"s1",
|
|
1950
|
+
expect.objectContaining({ type: "auth_status" }),
|
|
1951
|
+
);
|
|
1952
|
+
expect(deps.launcher.relaunch).not.toHaveBeenCalled();
|
|
1953
|
+
vi.useRealTimers();
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
it("skips auto-relaunch when the session is authBlocked (401 seen on the running CLI)", () => {
|
|
1957
|
+
vi.useFakeTimers();
|
|
1958
|
+
orchestrator.initialize();
|
|
1959
|
+
deps.launcher.getSession.mockReturnValue({ archived: false, state: "exited", pid: undefined } as any);
|
|
1960
|
+
deps.wsBridge.getSession.mockReturnValue({ authBlocked: true } as any);
|
|
1961
|
+
deps.wsBridge.isCliConnected.mockReturnValue(false);
|
|
1962
|
+
|
|
1963
|
+
// reason is a generic crash here — the skip is driven by session.authBlocked.
|
|
1964
|
+
companionBus.emit("session:exited", { sessionId: "s1", exitCode: 1, reason: "crash" });
|
|
1965
|
+
vi.advanceTimersByTime(60_000);
|
|
1966
|
+
|
|
1967
|
+
expect(deps.wsBridge.broadcastToSession).toHaveBeenCalledWith(
|
|
1968
|
+
"s1",
|
|
1969
|
+
expect.objectContaining({ type: "auth_status" }),
|
|
1970
|
+
);
|
|
1971
|
+
expect(deps.launcher.relaunch).not.toHaveBeenCalled();
|
|
1972
|
+
vi.useRealTimers();
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
// Regression: the PRIMARY auth signal (a 401 result on a still-running CLI)
|
|
1976
|
+
// sets session.authBlocked WITHOUT a process exit, so the guard is reached
|
|
1977
|
+
// via session:relaunch-needed (disconnect / browser-connect), not
|
|
1978
|
+
// session:exited. handleAutoRelaunch must honor authBlocked on this path too
|
|
1979
|
+
// or the --resume CLI loops straight back into the same expired credentials.
|
|
1980
|
+
it("skips auto-relaunch on the relaunch-needed path when authBlocked", async () => {
|
|
1981
|
+
vi.useFakeTimers();
|
|
1982
|
+
orchestrator.initialize();
|
|
1983
|
+
deps.launcher.getSession.mockReturnValue({ archived: false, state: "exited", pid: undefined } as any);
|
|
1984
|
+
deps.wsBridge.getSession.mockReturnValue({ authBlocked: true } as any);
|
|
1985
|
+
deps.wsBridge.isCliConnected.mockReturnValue(false);
|
|
1986
|
+
|
|
1987
|
+
companionBus.emit("session:relaunch-needed", { sessionId: "s1" });
|
|
1988
|
+
// Flush the async handler and any backoff window.
|
|
1989
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
1990
|
+
|
|
1991
|
+
expect(deps.wsBridge.broadcastToSession).toHaveBeenCalledWith(
|
|
1992
|
+
"s1",
|
|
1993
|
+
expect.objectContaining({ type: "auth_status" }),
|
|
1994
|
+
);
|
|
1995
|
+
expect(deps.launcher.relaunch).not.toHaveBeenCalled();
|
|
1996
|
+
vi.useRealTimers();
|
|
1997
|
+
});
|
|
1998
|
+
});
|
|
1934
1999
|
});
|
|
@@ -4,6 +4,7 @@ import type { SessionStore } from "./session-store.js";
|
|
|
4
4
|
import type { WorktreeTracker } from "./worktree-tracker.js";
|
|
5
5
|
import type { AgentExecutor } from "./agent-executor.js";
|
|
6
6
|
import type { BackendType, CreationStepId } from "./session-types.js";
|
|
7
|
+
import { AUTH_EXPIRED_MESSAGE } from "./session-types.js";
|
|
7
8
|
import type { ContainerConfig, ContainerInfo } from "./container-manager.js";
|
|
8
9
|
import { containerManager } from "./container-manager.js";
|
|
9
10
|
import { imagePullManager } from "./image-pull-manager.js";
|
|
@@ -82,6 +83,8 @@ export interface CreateSessionRequest {
|
|
|
82
83
|
backend?: string;
|
|
83
84
|
model?: string;
|
|
84
85
|
permissionMode?: string;
|
|
86
|
+
/** Reasoning-effort level (low|medium|high|xhigh|max). Empty/undefined = model default. Claude only. */
|
|
87
|
+
effort?: string;
|
|
85
88
|
cwd?: string;
|
|
86
89
|
claudeBinary?: string;
|
|
87
90
|
codexBinary?: string;
|
|
@@ -234,10 +237,26 @@ export class SessionOrchestrator {
|
|
|
234
237
|
// a browser connected. This ensures long-running sessions (agents, cron
|
|
235
238
|
// jobs) stay alive. Intentional kills (idle-kill, manual delete/archive)
|
|
236
239
|
// are excluded via the intentionalKills set.
|
|
237
|
-
companionBus.on("session:exited", ({ sessionId }) => {
|
|
240
|
+
companionBus.on("session:exited", ({ sessionId, reason }) => {
|
|
238
241
|
// The session died before earning its budget back — cancel the pending
|
|
239
242
|
// "healthy" reset so this crash counts toward MAX_AUTO_RELAUNCHES.
|
|
240
243
|
this.cancelHealthyReset(sessionId);
|
|
244
|
+
// Auth failures cannot be fixed by relaunching — a relaunched CLI just
|
|
245
|
+
// hits the same expired/invalid credentials and silently loops. Skip the
|
|
246
|
+
// keepalive and surface a re-login affordance instead. Detected either
|
|
247
|
+
// from the exit classification (reason) or from a 401 result observed on
|
|
248
|
+
// the still-running CLI before it exited (session.authBlocked).
|
|
249
|
+
const session = this.wsBridge.getSession(sessionId);
|
|
250
|
+
if (reason === "auth" || session?.authBlocked) {
|
|
251
|
+
log.warn("orchestrator", "Skipping auto-relaunch — authentication failure", { sessionId });
|
|
252
|
+
this.wsBridge.broadcastToSession(sessionId, {
|
|
253
|
+
type: "auth_status",
|
|
254
|
+
isAuthenticating: false,
|
|
255
|
+
output: [],
|
|
256
|
+
error: AUTH_EXPIRED_MESSAGE,
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
241
260
|
this.scheduleProactiveRelaunch(sessionId);
|
|
242
261
|
});
|
|
243
262
|
|
|
@@ -609,6 +628,7 @@ export class SessionOrchestrator {
|
|
|
609
628
|
session = this.launcher.launch({
|
|
610
629
|
model: body.model,
|
|
611
630
|
permissionMode: body.permissionMode,
|
|
631
|
+
effort: body.effort,
|
|
612
632
|
cwd,
|
|
613
633
|
claudeBinary: body.claudeBinary,
|
|
614
634
|
codexBinary: body.codexBinary,
|
|
@@ -819,6 +839,25 @@ export class SessionOrchestrator {
|
|
|
819
839
|
const info = this.launcher.getSession(sessionId);
|
|
820
840
|
if (info?.archived) return;
|
|
821
841
|
|
|
842
|
+
// Auth failures cannot be fixed by relaunching — a `--resume` CLI just hits
|
|
843
|
+
// the same expired/invalid credentials and loops. The PRIMARY auth signal is
|
|
844
|
+
// an auth-error `result` on a still-running CLI (no process exit), so the
|
|
845
|
+
// `session:exited` guard never sees it; when that CLI later disconnects, the
|
|
846
|
+
// relaunch is requested via `session:relaunch-needed` / browser-connect,
|
|
847
|
+
// which routes here. Honor session.authBlocked on this path too and surface
|
|
848
|
+
// a re-login affordance instead of churning relaunches. (This also covers
|
|
849
|
+
// the proactive-keepalive path, which delegates to handleAutoRelaunch.)
|
|
850
|
+
if (this.wsBridge.getSession(sessionId)?.authBlocked) {
|
|
851
|
+
log.warn("orchestrator", "Skipping auto-relaunch — authentication failure", { sessionId });
|
|
852
|
+
this.wsBridge.broadcastToSession(sessionId, {
|
|
853
|
+
type: "auth_status",
|
|
854
|
+
isAuthenticating: false,
|
|
855
|
+
output: [],
|
|
856
|
+
error: AUTH_EXPIRED_MESSAGE,
|
|
857
|
+
});
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
822
861
|
// If we've already notified the user about relaunch exhaustion, bail out
|
|
823
862
|
// silently. Without this, every reconnect event from a dead session
|
|
824
863
|
// (e.g. deleted container) re-logs the "limit reached" warning endlessly.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isAuthErrorResult, looksLikeAuthErrorText, AUTH_EXPIRED_MESSAGE } from "./session-types.js";
|
|
3
|
+
|
|
4
|
+
// These guard the auth-failure detection shared by the server (skip auto-relaunch
|
|
5
|
+
// + surface a re-login banner) and the client (error bubble). The key edge case
|
|
6
|
+
// is that the CLI reports auth failures with is_error:true but an EMPTY errors[]
|
|
7
|
+
// array — the human text lives in `result` / `api_error_status`.
|
|
8
|
+
describe("isAuthErrorResult", () => {
|
|
9
|
+
it("detects a 401 via api_error_status even when result text is absent", () => {
|
|
10
|
+
expect(isAuthErrorResult({ is_error: true, api_error_status: 401 })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("detects a 403 via api_error_status", () => {
|
|
14
|
+
expect(isAuthErrorResult({ is_error: true, api_error_status: 403 })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("detects the real CLI auth message via result text (no errors[], no status)", () => {
|
|
18
|
+
expect(
|
|
19
|
+
isAuthErrorResult({
|
|
20
|
+
is_error: true,
|
|
21
|
+
result: "Failed to authenticate. API Error: 401 Invalid authentication credentials",
|
|
22
|
+
}),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("detects 'please run /login' guidance", () => {
|
|
27
|
+
expect(
|
|
28
|
+
isAuthErrorResult({ is_error: true, result: "Session ended. Please run /login to continue." }),
|
|
29
|
+
).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("detects an expired-credentials phrase in errors[]", () => {
|
|
33
|
+
expect(
|
|
34
|
+
isAuthErrorResult({ is_error: true, errors: ["oauth token expired, please re-authenticate"] }),
|
|
35
|
+
).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("is false when is_error is not set, even with a 401 status", () => {
|
|
39
|
+
// A non-error result should never be treated as an auth failure.
|
|
40
|
+
expect(isAuthErrorResult({ is_error: false, api_error_status: 401 })).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is false for an ordinary (non-auth) error", () => {
|
|
44
|
+
expect(
|
|
45
|
+
isAuthErrorResult({ is_error: true, result: "Tool execution failed: ENOENT" }),
|
|
46
|
+
).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not misfire on a 4011 substring (word-boundary guarded)", () => {
|
|
50
|
+
// Ensure the \b401\b match doesn't fire on numbers that merely contain 401.
|
|
51
|
+
expect(isAuthErrorResult({ is_error: true, result: "processed 4011 records" })).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("exposes user-facing re-login guidance", () => {
|
|
55
|
+
expect(AUTH_EXPIRED_MESSAGE).toMatch(/claude login/i);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Regression: a bare 401/403/"unauthorized" appearing in unrelated error text
|
|
59
|
+
// must NOT be read as an auth failure. Misclassifying a recoverable crash as
|
|
60
|
+
// auth suppresses auto-relaunch and leaves the session dead — a new hang.
|
|
61
|
+
// These mirror real CLI stderr/result noise (stack-trace line numbers, tool
|
|
62
|
+
// exit codes, file-permission messages).
|
|
63
|
+
it.each([
|
|
64
|
+
"TypeError: at Object.<anonymous> (/app/dist/index.js:403:15)",
|
|
65
|
+
"test failed at line 401",
|
|
66
|
+
"The command exited with code 401",
|
|
67
|
+
"unauthorized to delete this file",
|
|
68
|
+
"RangeError [ERR_OUT_OF_RANGE]: offset 403 is out of range",
|
|
69
|
+
"processed 4011 records",
|
|
70
|
+
])("does NOT classify non-auth text as auth: %s", (text) => {
|
|
71
|
+
expect(looksLikeAuthErrorText(text)).toBe(false);
|
|
72
|
+
expect(isAuthErrorResult({ is_error: true, result: text })).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ...but a status code paired with the auth word, or an explicit auth phrase,
|
|
76
|
+
// is still recognized.
|
|
77
|
+
it.each([
|
|
78
|
+
"API Error: 401 Unauthorized",
|
|
79
|
+
"403 Forbidden",
|
|
80
|
+
"Invalid authentication credentials",
|
|
81
|
+
"Please run /login",
|
|
82
|
+
"oauth token expired",
|
|
83
|
+
])("still classifies genuine auth text as auth: %s", (text) => {
|
|
84
|
+
expect(looksLikeAuthErrorText(text)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|