@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.
Files changed (28) hide show
  1. package/dist/assets/{AgentsPage-H1WOCJ_U.js → AgentsPage-C_uaskvu.js} +1 -1
  2. package/dist/assets/{CronManager-Cqnqj1KO.js → CronManager-_ScZYZUL.js} +1 -1
  3. package/dist/assets/{IntegrationsPage-Nd6In9Av.js → IntegrationsPage-j39LFDWj.js} +1 -1
  4. package/dist/assets/{LinearOAuthSettingsPage-D7RQfyFI.js → LinearOAuthSettingsPage-B437XSi7.js} +1 -1
  5. package/dist/assets/{LinearSettingsPage-CGVs5jG6.js → LinearSettingsPage-CrHvZJ2l.js} +1 -1
  6. package/dist/assets/{Playground-M3TI9s_p.js → Playground-Vo-LT0XD.js} +1 -1
  7. package/dist/assets/{PromptsPage-QXhg1mXT.js → PromptsPage-CBFfssun.js} +1 -1
  8. package/dist/assets/{RunsPage-C1XiccgQ.js → RunsPage-CzkUlLNh.js} +1 -1
  9. package/dist/assets/{SandboxManager-DYmKXcTM.js → SandboxManager-F4OKDTsR.js} +1 -1
  10. package/dist/assets/{SettingsPage-DaJfAHDI.js → SettingsPage-BMOYx-nn.js} +1 -1
  11. package/dist/assets/{TailscalePage-CmH09EMI.js → TailscalePage-Cs98ENqe.js} +1 -1
  12. package/dist/assets/index-BkJgPfVV.js +136 -0
  13. package/dist/assets/{sw-register-DkUWP5b0.js → sw-register-CNrT7Vew.js} +1 -1
  14. package/dist/index.html +1 -1
  15. package/dist/sw.js +1 -1
  16. package/package.json +1 -1
  17. package/server/claude-adapter.ts +18 -8
  18. package/server/cli-launcher.test.ts +92 -0
  19. package/server/cli-launcher.ts +54 -1
  20. package/server/event-bus-types.ts +10 -2
  21. package/server/session-orchestrator.test.ts +65 -0
  22. package/server/session-orchestrator.ts +40 -1
  23. package/server/session-types.test.ts +86 -0
  24. package/server/session-types.ts +57 -0
  25. package/server/ws-bridge-types.ts +20 -0
  26. package/server/ws-bridge.test.ts +178 -0
  27. package/server/ws-bridge.ts +59 -0
  28. package/dist/assets/index-CKx0s4-A.js +0 -134
@@ -1 +1 @@
1
- import{_ as u}from"./index-CKx0s4-A.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")}});
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-CKx0s4-A.js"></script>
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":"db0bd8f0032bf19eae29a0b176ceb4cc","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-DkUWP5b0.js"},{"revision":null,"url":"assets/index-psWeuMrk.css"},{"revision":null,"url":"assets/index-CKx0s4-A.js"},{"revision":null,"url":"assets/TailscalePage-CmH09EMI.js"},{"revision":null,"url":"assets/SettingsPage-DaJfAHDI.js"},{"revision":null,"url":"assets/SandboxManager-DYmKXcTM.js"},{"revision":null,"url":"assets/RunsPage-C1XiccgQ.js"},{"revision":null,"url":"assets/PromptsPage-QXhg1mXT.js"},{"revision":null,"url":"assets/Playground-M3TI9s_p.js"},{"revision":null,"url":"assets/LinearSettingsPage-CGVs5jG6.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-D7RQfyFI.js"},{"revision":null,"url":"assets/IntegrationsPage-Nd6In9Av.js"},{"revision":null,"url":"assets/CronManager-Cqnqj1KO.js"},{"revision":null,"url":"assets/AgentsPage-H1WOCJ_U.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
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",
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",
@@ -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
- this.sendToBackend(ndjson);
564
- return true;
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): void {
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
- return;
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): void {
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
- this.stdioWriter?.write(new TextEncoder().encode(ndjson + "\n"));
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!.send(ndjson + "\n");
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
+ });
@@ -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
- companionBus.emit("session:exited", { sessionId, exitCode });
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
- /** CLI/Codex process exited. */
16
- "session:exited": { sessionId: string; exitCode: number | null };
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
+ });