@hellcoder/companion 0.112.1-preview.20260728092957.044121e → 0.113.0

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 (23) hide show
  1. package/dist/assets/{AgentsPage-D17hofXo.js → AgentsPage-BBi5x0sS.js} +1 -1
  2. package/dist/assets/{CronManager-YyyPK6hb.js → CronManager-B1s3pGHK.js} +1 -1
  3. package/dist/assets/{DashboardPage-ukAmlbn5.js → DashboardPage-DFgvPALP.js} +1 -1
  4. package/dist/assets/{IntegrationsPage-C5rUwiN9.js → IntegrationsPage-ClHcXTNR.js} +1 -1
  5. package/dist/assets/{LinearOAuthSettingsPage-6OAQZdqq.js → LinearOAuthSettingsPage-aKHb8Kgv.js} +1 -1
  6. package/dist/assets/{LinearSettingsPage-BQSZm4Bq.js → LinearSettingsPage-DZbV5ES0.js} +1 -1
  7. package/dist/assets/{Playground-Bq0Tks1d.js → Playground-Z8gfp_Pg.js} +2 -2
  8. package/dist/assets/{PromptsPage-Bz9Ml49y.js → PromptsPage-BF5QRCM7.js} +1 -1
  9. package/dist/assets/{RunsPage-CxlLKCM8.js → RunsPage-Bn7SIAEx.js} +1 -1
  10. package/dist/assets/{SandboxManager-VUkfrmv0.js → SandboxManager-BNIPWLHz.js} +1 -1
  11. package/dist/assets/{SettingsPage-BL8vCPyR.js → SettingsPage-CFe6_dW9.js} +1 -1
  12. package/dist/assets/{TailscalePage-DvlPwchP.js → TailscalePage-DPJGhCUO.js} +1 -1
  13. package/dist/assets/{index-S0nvfWUo.js → index-B6xqr-v6.js} +44 -44
  14. package/dist/assets/{sw-register-CZ9DmSD-.js → sw-register-A0FMc-CY.js} +1 -1
  15. package/dist/index.html +1 -1
  16. package/dist/sw.js +1 -1
  17. package/package.json +1 -1
  18. package/server/claude-adapter.test.ts +169 -0
  19. package/server/claude-adapter.ts +53 -3
  20. package/server/cli-launcher.ts +2 -0
  21. package/server/session-types.ts +26 -1
  22. package/server/ws-bridge.test.ts +44 -0
  23. package/server/ws-bridge.ts +12 -4
@@ -1 +1 @@
1
- import{_ as u}from"./index-S0nvfWUo.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-B6xqr-v6.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-S0nvfWUo.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-B6xqr-v6.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-C86AT0KJ.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":"d25d768699a108499b7a2b21180c1be0","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-CZ9DmSD-.js"},{"revision":null,"url":"assets/index-S0nvfWUo.js"},{"revision":null,"url":"assets/index-C86AT0KJ.css"},{"revision":null,"url":"assets/TailscalePage-DvlPwchP.js"},{"revision":null,"url":"assets/SettingsPage-BL8vCPyR.js"},{"revision":null,"url":"assets/SandboxManager-VUkfrmv0.js"},{"revision":null,"url":"assets/RunsPage-CxlLKCM8.js"},{"revision":null,"url":"assets/PromptsPage-Bz9Ml49y.js"},{"revision":null,"url":"assets/Playground-Bq0Tks1d.js"},{"revision":null,"url":"assets/LinearSettingsPage-BQSZm4Bq.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-6OAQZdqq.js"},{"revision":null,"url":"assets/IntegrationsPage-C5rUwiN9.js"},{"revision":null,"url":"assets/DashboardPage-ukAmlbn5.js"},{"revision":null,"url":"assets/CronManager-YyyPK6hb.js"},{"revision":null,"url":"assets/AgentsPage-D17hofXo.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":"4f8bb842832a3076ea5fd36837194001","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-A0FMc-CY.js"},{"revision":null,"url":"assets/index-C86AT0KJ.css"},{"revision":null,"url":"assets/index-B6xqr-v6.js"},{"revision":null,"url":"assets/TailscalePage-DPJGhCUO.js"},{"revision":null,"url":"assets/SettingsPage-CFe6_dW9.js"},{"revision":null,"url":"assets/SandboxManager-BNIPWLHz.js"},{"revision":null,"url":"assets/RunsPage-Bn7SIAEx.js"},{"revision":null,"url":"assets/PromptsPage-BF5QRCM7.js"},{"revision":null,"url":"assets/Playground-Z8gfp_Pg.js"},{"revision":null,"url":"assets/LinearSettingsPage-DZbV5ES0.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-aKHb8Kgv.js"},{"revision":null,"url":"assets/IntegrationsPage-ClHcXTNR.js"},{"revision":null,"url":"assets/DashboardPage-DFgvPALP.js"},{"revision":null,"url":"assets/CronManager-B1s3pGHK.js"},{"revision":null,"url":"assets/AgentsPage-BBi5x0sS.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.112.1-preview.20260728092957.044121e",
3
+ "version": "0.113.0",
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",
@@ -55,6 +55,7 @@ vi.mock("./settings-manager.js", () => ({
55
55
  }));
56
56
 
57
57
  import { ClaudeAdapter } from "./claude-adapter.js";
58
+ import type { BrowserIncomingMessage } from "./session-types.js";
58
59
  import { log } from "./logger.js";
59
60
 
60
61
  // ─── Mock socket factory ────────────────────────────────────────────────────
@@ -2221,3 +2222,171 @@ describe("orphaned descendant reaping", () => {
2221
2222
  expect(signalled).toEqual([]);
2222
2223
  });
2223
2224
  });
2225
+
2226
+ /**
2227
+ * stderr on the kill path.
2228
+ *
2229
+ * The launcher already captures a rolling stderr tail to classify *exit*
2230
+ * reasons, but the wedge path kills the process rather than letting it exit, so
2231
+ * that buffer was never surfaced. Every wedge warning therefore reported kernel
2232
+ * state (S/ep_poll, RSS, fds) and nothing about what the CLI itself said before
2233
+ * dropping stdout — which is the one piece of evidence that could explain a
2234
+ * wedge rather than merely report it. In production 35 of 36 kills were
2235
+ * `no_descendants` on healthy sleeping processes with no explanation available.
2236
+ */
2237
+ describe("stderr tail on kill warnings", () => {
2238
+ let warnSpy: ReturnType<typeof vi.spyOn>;
2239
+
2240
+ beforeEach(() => {
2241
+ warnSpy = vi.spyOn(log, "warn").mockImplementation(() => {});
2242
+ });
2243
+
2244
+ afterEach(() => {
2245
+ warnSpy.mockRestore();
2246
+ vi.useRealTimers();
2247
+ });
2248
+
2249
+ const wedgeWarning = () =>
2250
+ warnSpy.mock.calls.find((c: unknown[]) => String(c[1]).includes("killing wedged process"));
2251
+
2252
+ it("includes the CLI's stderr in the wedge-kill warning", async () => {
2253
+ vi.useFakeTimers();
2254
+ const adapter = new ClaudeAdapter("stderr-session", {
2255
+ getStderrTail: () => "FATAL: upstream stream closed unexpectedly",
2256
+ });
2257
+ const { proc, endStdout } = createMockProc();
2258
+ adapter.attachStdio(proc);
2259
+
2260
+ endStdout();
2261
+ await vi.advanceTimersByTimeAsync(11_000);
2262
+
2263
+ const call = wedgeWarning();
2264
+ expect(call).toBeDefined();
2265
+ expect(call?.[2]).toMatchObject({ stderrTail: "FATAL: upstream stream closed unexpectedly" });
2266
+ });
2267
+
2268
+ it("omits the field entirely when the CLI said nothing", async () => {
2269
+ // A quiet failure should stay quiet rather than log an empty string.
2270
+ vi.useFakeTimers();
2271
+ const adapter = new ClaudeAdapter("stderr-quiet", { getStderrTail: () => " " });
2272
+ const { proc, endStdout } = createMockProc();
2273
+ adapter.attachStdio(proc);
2274
+
2275
+ endStdout();
2276
+ await vi.advanceTimersByTimeAsync(11_000);
2277
+
2278
+ expect(wedgeWarning()?.[2]).toMatchObject({ stderrTail: undefined });
2279
+ });
2280
+
2281
+ it("caps a chatty process so it cannot flood the log", async () => {
2282
+ vi.useFakeTimers();
2283
+ const adapter = new ClaudeAdapter("stderr-loud", { getStderrTail: () => "x".repeat(5000) });
2284
+ const { proc, endStdout } = createMockProc();
2285
+ adapter.attachStdio(proc);
2286
+
2287
+ endStdout();
2288
+ await vi.advanceTimersByTimeAsync(11_000);
2289
+
2290
+ const tail = (wedgeWarning()?.[2] as { stderrTail?: string })?.stderrTail;
2291
+ expect(tail).toHaveLength(600);
2292
+ });
2293
+
2294
+ it("still kills when the stderr reader throws", async () => {
2295
+ // Diagnostics must never be able to block recovery.
2296
+ vi.useFakeTimers();
2297
+ const adapter = new ClaudeAdapter("stderr-throws", {
2298
+ getStderrTail: () => { throw new Error("launcher gone"); },
2299
+ });
2300
+ const { proc, endStdout } = createMockProc();
2301
+ adapter.attachStdio(proc);
2302
+
2303
+ endStdout();
2304
+ await vi.advanceTimersByTimeAsync(11_000);
2305
+
2306
+ expect(proc.kill).toHaveBeenCalled();
2307
+ expect(wedgeWarning()?.[2]).toMatchObject({ stderrTail: undefined });
2308
+ });
2309
+
2310
+ it("works when no stderr reader is supplied at all", async () => {
2311
+ vi.useFakeTimers();
2312
+ const adapter = new ClaudeAdapter("stderr-absent");
2313
+ const { proc, endStdout } = createMockProc();
2314
+ adapter.attachStdio(proc);
2315
+
2316
+ endStdout();
2317
+ await vi.advanceTimersByTimeAsync(11_000);
2318
+
2319
+ expect(proc.kill).toHaveBeenCalled();
2320
+ expect(wedgeWarning()).toBeDefined();
2321
+ });
2322
+ });
2323
+
2324
+ /**
2325
+ * rate_limit_event was previously discarded with the comment "no user-facing
2326
+ * action needed". Recorded traffic disagrees: across 200 events there were 14
2327
+ * `allowed_warning` and 3 `rejected`, none of which reached the UI or the logs.
2328
+ * It is also the only signal we get about plan-window usage.
2329
+ */
2330
+ describe("rate_limit_event surfacing", () => {
2331
+ it("forwards the info to the browser", async () => {
2332
+ const emitted: BrowserIncomingMessage[] = [];
2333
+ const a = new ClaudeAdapter("rl-1");
2334
+ a.onBrowserMessage((m) => emitted.push(m));
2335
+ const { proc, pushStdout } = createMockProc();
2336
+ a.attachStdio(proc);
2337
+
2338
+ pushStdout(JSON.stringify({
2339
+ type: "rate_limit_event",
2340
+ rate_limit_info: {
2341
+ status: "allowed_warning",
2342
+ utilization: 0.95,
2343
+ rateLimitType: "five_hour",
2344
+ resetsAt: 1784441400,
2345
+ },
2346
+ uuid: "rl-uuid-1",
2347
+ session_id: "s",
2348
+ }) + "\n");
2349
+ await new Promise((r) => setTimeout(r, 20));
2350
+
2351
+ const msg = emitted.find((m) => m.type === "rate_limit") as
2352
+ | { type: "rate_limit"; info: Record<string, unknown> }
2353
+ | undefined;
2354
+ expect(msg).toBeDefined();
2355
+ expect(msg?.info.status).toBe("allowed_warning");
2356
+ // Utilization must survive intact — it drives the meter fill.
2357
+ expect(msg?.info.utilization).toBe(0.95);
2358
+ });
2359
+
2360
+ it("still forwards allowed events so the UI can clear a stale warning", async () => {
2361
+ // If only warnings were forwarded, a session would stay visually throttled
2362
+ // after recovering.
2363
+ const emitted: BrowserIncomingMessage[] = [];
2364
+ const a = new ClaudeAdapter("rl-2");
2365
+ a.onBrowserMessage((m) => emitted.push(m));
2366
+ const { proc, pushStdout } = createMockProc();
2367
+ a.attachStdio(proc);
2368
+
2369
+ pushStdout(JSON.stringify({
2370
+ type: "rate_limit_event",
2371
+ rate_limit_info: { status: "allowed", rateLimitType: "five_hour" },
2372
+ session_id: "s",
2373
+ }) + "\n");
2374
+ await new Promise((r) => setTimeout(r, 20));
2375
+
2376
+ expect(emitted.some((m) => m.type === "rate_limit")).toBe(true);
2377
+ });
2378
+
2379
+ it("tolerates a missing rate_limit_info without throwing", async () => {
2380
+ // A throw here would kill the stdout reader for the life of the process —
2381
+ // the exact failure the e2e harness hit with Bun.hash.
2382
+ const a = new ClaudeAdapter("rl-3");
2383
+ const { proc, pushStdout } = createMockProc();
2384
+ a.attachStdio(proc);
2385
+
2386
+ expect(() => {
2387
+ pushStdout(JSON.stringify({ type: "rate_limit_event", session_id: "s" }) + "\n");
2388
+ }).not.toThrow();
2389
+ await new Promise((r) => setTimeout(r, 20));
2390
+ expect(a.isConnected()).toBe(true);
2391
+ });
2392
+ });
@@ -30,6 +30,7 @@ import type {
30
30
  CLIControlResponseMessage,
31
31
  CLIAuthStatusMessage,
32
32
  CLIControlCancelRequestMessage,
33
+ CLIRateLimitEventMessage,
33
34
  CLIStreamlinedTextMessage,
34
35
  CLIStreamlinedToolUseSummaryMessage,
35
36
  CLIPromptSuggestionMessage,
@@ -241,6 +242,7 @@ export class ClaudeAdapter implements IBackendAdapter {
241
242
  * must be killed so recovery can proceed). Transport-level noise
242
243
  * (`keep_alive`, `system` status/keepalive) does not reset it; any
243
244
  * substantive turn activity does. */
245
+ private getStderrTail: (() => string) | null = null;
244
246
  private lastInboundWasResult = false;
245
247
  /** Timestamp of the last inbound frame of any kind, for the silence probe. */
246
248
  private lastInboundAt = Date.now();
@@ -304,10 +306,19 @@ export class ClaudeAdapter implements IBackendAdapter {
304
306
  * `<cwd>/.companion-uploads/`. Optional because tests may construct
305
307
  * without it; the bridge should always pass it. */
306
308
  cwd?: string;
309
+ /** Reads the rolling stderr tail for this session.
310
+ *
311
+ * The launcher already captures stderr to classify exit reasons, but on
312
+ * the wedge path — where the process is killed rather than exiting — that
313
+ * buffer was never surfaced. It is the only record of what the CLI said
314
+ * before it dropped its stdout, so it is the one piece of evidence that
315
+ * can explain a wedge rather than just report it. */
316
+ getStderrTail?: () => string;
307
317
  },
308
318
  ) {
309
319
  this.sessionId = sessionId;
310
320
  this.recorder = opts?.recorder ?? null;
321
+ this.getStderrTail = opts?.getStderrTail ?? null;
311
322
  this.onActivityUpdate = opts?.onActivityUpdate ?? null;
312
323
  this.sessionCwd = opts?.cwd ?? null;
313
324
  }
@@ -489,6 +500,7 @@ export class ClaudeAdapter implements IBackendAdapter {
489
500
  sessionId: this.sessionId,
490
501
  pid: proc.pid,
491
502
  proc: captureProcState(proc.pid),
503
+ stderrTail: this.stderrTailForLog(),
492
504
  });
493
505
  } else if (proc && proc.exitCode === null && !proc.killed) {
494
506
  // Which grace applies is decided by whether teardown is actually in
@@ -527,6 +539,7 @@ export class ClaudeAdapter implements IBackendAdapter {
527
539
  graceReason: this.lastInboundWasResult ? "result" : "descendants_alive",
528
540
  teardownOutcome: this.lastTeardownOutcome,
529
541
  proc: captureProcState(proc.pid),
542
+ stderrTail: this.stderrTailForLog(),
530
543
  });
531
544
  await this.killWithEscalation(proc);
532
545
  }
@@ -553,6 +566,7 @@ export class ClaudeAdapter implements IBackendAdapter {
553
566
  graceMs: STDOUT_CLOSE_GRACE_MS,
554
567
  graceReason: "no_descendants",
555
568
  proc: captureProcState(proc.pid),
569
+ stderrTail: this.stderrTailForLog(),
556
570
  });
557
571
  await this.killWithEscalation(proc);
558
572
  }
@@ -608,6 +622,21 @@ export class ClaudeAdapter implements IBackendAdapter {
608
622
  for (const id of results) this.pendingToolUses.delete(id);
609
623
  }
610
624
 
625
+ /**
626
+ * Last few hundred bytes the CLI wrote to stderr, for kill/stall warnings.
627
+ *
628
+ * Trimmed and length-capped so a chatty process cannot flood the log, and
629
+ * omitted entirely when empty so quiet failures stay quiet.
630
+ */
631
+ private stderrTailForLog(): string | undefined {
632
+ try {
633
+ const tail = this.getStderrTail?.().trim();
634
+ return tail ? tail.slice(-600) : undefined;
635
+ } catch {
636
+ return undefined;
637
+ }
638
+ }
639
+
611
640
  private checkSilence(): void {
612
641
  if (this.transportKind !== "stdio" || !this.stdioConnected) return;
613
642
  if (getSettings().silenceProbeEnabled === false) return;
@@ -628,6 +657,7 @@ export class ClaudeAdapter implements IBackendAdapter {
628
657
  turnSilentForMs: now - this.lastTurnOutputAt,
629
658
  transportSilentForMs: now - this.lastInboundAt,
630
659
  proc: captureProcState(this.stdioProc?.pid),
660
+ stderrTail: this.stderrTailForLog(),
631
661
  });
632
662
  this.stopSilenceProbe();
633
663
  // Same handoff as the silence probe: relaunch replays the in-flight turn.
@@ -661,6 +691,7 @@ export class ClaudeAdapter implements IBackendAdapter {
661
691
  silentForMs: now - this.lastInboundAt,
662
692
  probeUnansweredForMs: now - this.probeSentAt,
663
693
  proc: captureProcState(this.stdioProc?.pid),
694
+ stderrTail: this.stderrTailForLog(),
664
695
  });
665
696
  this.stopSilenceProbe();
666
697
  // Hand off to the existing recovery path, which relaunches and replays
@@ -787,6 +818,7 @@ export class ClaudeAdapter implements IBackendAdapter {
787
818
  sessionId: this.sessionId,
788
819
  pid: proc.pid,
789
820
  afterMs: SIGKILL_ESCALATION_MS,
821
+ stderrTail: this.stderrTailForLog(),
790
822
  });
791
823
  try {
792
824
  proc.kill("SIGKILL");
@@ -1363,10 +1395,28 @@ export class ClaudeAdapter implements IBackendAdapter {
1363
1395
  // to avoid rendering raw tool_result JSON in the chat UI.
1364
1396
  break;
1365
1397
 
1366
- case "rate_limit_event":
1367
- // Rate-limit status from Claude API (allowed/throttled). Silently
1368
- // consumed no user-facing action needed.
1398
+ case "rate_limit_event": {
1399
+ // Previously discarded. It is the only signal we get about plan-window
1400
+ // usage, and recorded traffic shows it does fire in anger (14 warning
1401
+ // and 3 rejected events across 200 samples) — while the UI said nothing
1402
+ // and the logs kept no record.
1403
+ const info = (msg as CLIRateLimitEventMessage).rate_limit_info ?? {};
1404
+ // Only non-`allowed` states are logged: `allowed` arrives constantly and
1405
+ // would drown the log, whereas a warning or rejection is exactly what a
1406
+ // later investigation needs to find.
1407
+ if (info.status && info.status !== "allowed") {
1408
+ log.warn("claude-adapter", "API rate limit", {
1409
+ sessionId: this.sessionId,
1410
+ status: info.status,
1411
+ utilization: info.utilization,
1412
+ rateLimitType: info.rateLimitType,
1413
+ resetsAt: info.resetsAt,
1414
+ isUsingOverage: info.isUsingOverage,
1415
+ });
1416
+ }
1417
+ this.browserMessageCb?.({ type: "rate_limit", info });
1369
1418
  break;
1419
+ }
1370
1420
 
1371
1421
  case "control_cancel_request":
1372
1422
  this.handleControlCancelRequest(msg as CLIControlCancelRequestMessage);
@@ -633,6 +633,8 @@ export class CliLauncher {
633
633
  const adapter = new ClaudeAdapter(sessionId, {
634
634
  recorder: this.recorder ?? undefined,
635
635
  cwd: info.cwd,
636
+ // Lets the adapter quote the CLI's own stderr in its kill/stall warnings.
637
+ getStderrTail: () => this.stderrTails.get(sessionId) ?? "",
636
638
  });
637
639
  adapter.attachStdio(proc);
638
640
  companionBus.emit("backend:claude-adapter-created", { sessionId, adapter });
@@ -297,10 +297,34 @@ export interface CLIUserEchoMessage {
297
297
  session_id?: string;
298
298
  }
299
299
 
300
+ /**
301
+ * Rate-limit status from the Claude API.
302
+ *
303
+ * Field shapes verified against 200 recorded events: status was `allowed`
304
+ * (183), `allowed_warning` (14) and `rejected` (3); `utilization` and
305
+ * `surpassedThreshold` appear on warning events, and `rateLimitType` was
306
+ * always `five_hour` — the plan window, not the per-minute RPM/TPM cap.
307
+ */
308
+ export interface RateLimitInfo {
309
+ /** `allowed` | `allowed_warning` | `rejected`, per observed traffic. */
310
+ status?: string;
311
+ /** Fraction of the window consumed, 0-1 (e.g. 0.95). Warning events only. */
312
+ utilization?: number;
313
+ /** The threshold that tripped this warning, 0-1 (e.g. 0.9). */
314
+ surpassedThreshold?: number;
315
+ /** Unix seconds at which the window resets. */
316
+ resetsAt?: number;
317
+ /** e.g. "five_hour". */
318
+ rateLimitType?: string;
319
+ overageStatus?: string;
320
+ overageDisabledReason?: string;
321
+ isUsingOverage?: boolean;
322
+ }
323
+
300
324
  /** Rate-limit status from Claude API (allowed/throttled). */
301
325
  export interface CLIRateLimitEventMessage {
302
326
  type: "rate_limit_event";
303
- rate_limit_info: Record<string, unknown>;
327
+ rate_limit_info: RateLimitInfo & Record<string, unknown>;
304
328
  uuid?: string;
305
329
  }
306
330
 
@@ -414,6 +438,7 @@ export type BrowserIncomingMessageBase =
414
438
  | { type: "session_name_update"; name: string }
415
439
  | { type: "pr_status_update"; pr: import("./github-pr.js").GitHubPRInfo | null; available: boolean }
416
440
  | { type: "mcp_status"; servers: McpServerDetail[] }
441
+ | { type: "rate_limit"; info: RateLimitInfo }
417
442
  | { type: "session_phase"; phase: SessionPhase; previousPhase: SessionPhase }
418
443
  | { type: "prompt_suggestion"; suggestions: string[] }
419
444
  | { type: "streamlined_text"; text: string }
@@ -1996,6 +1996,50 @@ describe("CLI message routing", () => {
1996
1996
  }
1997
1997
  });
1998
1998
 
1999
+ /**
2000
+ * Regression: the server-owned-adapter disconnect path serves BOTH stdio
2001
+ * Claude and Codex, but every label on it said "Codex" — so a Claude
2002
+ * session's disconnects were filed under the wrong backend and any forensic
2003
+ * search for them looked under the wrong term. Observed live:
2004
+ * reason=codex_disconnect_confirmed backendType=claude
2005
+ */
2006
+ it("labels adapter disconnects with the session's actual backend", async () => {
2007
+ vi.useFakeTimers();
2008
+ try {
2009
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
2010
+ const session = bridge.getOrCreateSession("s1");
2011
+ expect(session.backendType).toBe("claude");
2012
+
2013
+ const browser = makeBrowserSocket("s1");
2014
+ bridge.handleBrowserOpen(browser, "s1");
2015
+
2016
+ let disconnectCb: (() => void) | undefined;
2017
+ const adapter = {
2018
+ isConnected: () => false,
2019
+ send: () => true,
2020
+ disconnect: async () => {},
2021
+ onBrowserMessage: () => {},
2022
+ onSessionMeta: () => {},
2023
+ onDisconnect: (cb: () => void) => { disconnectCb = cb; },
2024
+ onInitError: () => {},
2025
+ };
2026
+ bridge.attachBackendAdapter("s1", adapter as never, "claude");
2027
+
2028
+ disconnectCb!();
2029
+ await vi.advanceTimersByTimeAsync(10_000);
2030
+
2031
+ const banner = warnSpy.mock.calls
2032
+ .map(([a]) => String(a))
2033
+ .find((l) => l.includes("UI: CLI disconnected banner shown"));
2034
+ expect(banner).toBeDefined();
2035
+ // A Claude session must never be filed under codex_*.
2036
+ expect(banner).toContain("claude_disconnect_confirmed");
2037
+ expect(banner).not.toContain("codex_disconnect_confirmed");
2038
+ } finally {
2039
+ vi.useRealTimers();
2040
+ }
2041
+ });
2042
+
1999
2043
  it("tool_progress: broadcasts", async () => {
2000
2044
  const msg = JSON.stringify({
2001
2045
  type: "tool_progress",
@@ -669,9 +669,17 @@ export class WsBridge {
669
669
  // "reconnecting" with a short debounce (5s vs 15s for the legacy WS
670
670
  // Claude transport, since there's no WebSocket cycling).
671
671
  session.backendAdapter = null;
672
- session.stateMachine.transition("reconnecting", "codex_adapter_disconnected");
672
+ // This path serves BOTH stdio Claude and Codex, but every label here used
673
+ // to say "Codex" — so a Claude session's disconnects were filed under the
674
+ // wrong backend in the logs, and forensics silently searched the wrong
675
+ // term. Label from the session's actual backend instead.
676
+ const backend = session.backendType;
677
+ session.stateMachine.transition("reconnecting", `${backend}_adapter_disconnected`);
673
678
  this.persistSession(session);
674
- log.info("ws-bridge", "Codex adapter disconnected, starting debounce", { sessionId });
679
+ log.info("ws-bridge", "Backend adapter disconnected, starting debounce", {
680
+ sessionId,
681
+ backendType: backend,
682
+ });
675
683
 
676
684
  const existing = this.disconnectTimers.get(sessionId);
677
685
  if (existing) clearTimeout(existing);
@@ -680,14 +688,14 @@ export class WsBridge {
680
688
  // Check if a new adapter reconnected during the grace period
681
689
  if (session.backendAdapter?.isConnected()) return;
682
690
 
683
- log.warn("ws-bridge", "Codex disconnect confirmed", { sessionId });
691
+ log.warn("ws-bridge", "Backend disconnect confirmed", { sessionId, backendType: backend });
684
692
  for (const [reqId] of session.pendingPermissions) {
685
693
  this.broadcastToBrowsers(session, { type: "permission_cancelled", request_id: reqId });
686
694
  }
687
695
  session.pendingPermissions.clear();
688
696
  session.stateMachine.transition("terminated", "disconnect_confirmed");
689
697
  this.persistSession(session);
690
- this.broadcastCliDisconnected(session, "codex_disconnect_confirmed");
698
+ this.broadcastCliDisconnected(session, `${backend}_disconnect_confirmed`);
691
699
 
692
700
  // Request auto-relaunch regardless of browser state — proactive
693
701
  // keepalive in the orchestrator ensures headless sessions stay alive.