@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.
- package/dist/assets/{AgentsPage-D17hofXo.js → AgentsPage-BBi5x0sS.js} +1 -1
- package/dist/assets/{CronManager-YyyPK6hb.js → CronManager-B1s3pGHK.js} +1 -1
- package/dist/assets/{DashboardPage-ukAmlbn5.js → DashboardPage-DFgvPALP.js} +1 -1
- package/dist/assets/{IntegrationsPage-C5rUwiN9.js → IntegrationsPage-ClHcXTNR.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-6OAQZdqq.js → LinearOAuthSettingsPage-aKHb8Kgv.js} +1 -1
- package/dist/assets/{LinearSettingsPage-BQSZm4Bq.js → LinearSettingsPage-DZbV5ES0.js} +1 -1
- package/dist/assets/{Playground-Bq0Tks1d.js → Playground-Z8gfp_Pg.js} +2 -2
- package/dist/assets/{PromptsPage-Bz9Ml49y.js → PromptsPage-BF5QRCM7.js} +1 -1
- package/dist/assets/{RunsPage-CxlLKCM8.js → RunsPage-Bn7SIAEx.js} +1 -1
- package/dist/assets/{SandboxManager-VUkfrmv0.js → SandboxManager-BNIPWLHz.js} +1 -1
- package/dist/assets/{SettingsPage-BL8vCPyR.js → SettingsPage-CFe6_dW9.js} +1 -1
- package/dist/assets/{TailscalePage-DvlPwchP.js → TailscalePage-DPJGhCUO.js} +1 -1
- package/dist/assets/{index-S0nvfWUo.js → index-B6xqr-v6.js} +44 -44
- package/dist/assets/{sw-register-CZ9DmSD-.js → sw-register-A0FMc-CY.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/claude-adapter.test.ts +169 -0
- package/server/claude-adapter.ts +53 -3
- package/server/cli-launcher.ts +2 -0
- package/server/session-types.ts +26 -1
- package/server/ws-bridge.test.ts +44 -0
- package/server/ws-bridge.ts +12 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
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-
|
|
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":"
|
|
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.
|
|
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
|
+
});
|
package/server/claude-adapter.ts
CHANGED
|
@@ -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
|
-
//
|
|
1368
|
-
//
|
|
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);
|
package/server/cli-launcher.ts
CHANGED
|
@@ -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 });
|
package/server/session-types.ts
CHANGED
|
@@ -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 }
|
package/server/ws-bridge.test.ts
CHANGED
|
@@ -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",
|
package/server/ws-bridge.ts
CHANGED
|
@@ -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
|
-
|
|
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", "
|
|
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", "
|
|
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,
|
|
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.
|