@hellcoder/companion 0.111.0 → 0.111.1
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-DO5MiZyp.js → AgentsPage-Dc23pQBM.js} +1 -1
- package/dist/assets/{CronManager-BSNkRBGn.js → CronManager-CKvnO3df.js} +1 -1
- package/dist/assets/{DashboardPage-DqwmmyJ6.js → DashboardPage-_gU5oXmV.js} +1 -1
- package/dist/assets/{IntegrationsPage-CxavpMnn.js → IntegrationsPage-zBdnuEDO.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-Bs_FXqPA.js → LinearOAuthSettingsPage-v4LYNLyW.js} +1 -1
- package/dist/assets/{LinearSettingsPage-zerN3EmK.js → LinearSettingsPage-C-Gm_aeW.js} +1 -1
- package/dist/assets/{Playground-DNlOjOnC.js → Playground-BheM7iin.js} +1 -1
- package/dist/assets/{PromptsPage-D20LKwla.js → PromptsPage-Bt2idDR8.js} +1 -1
- package/dist/assets/{RunsPage-ho-uEj6h.js → RunsPage-ib31kftd.js} +1 -1
- package/dist/assets/{SandboxManager-BY2NPfsU.js → SandboxManager-Ch0-1iE8.js} +1 -1
- package/dist/assets/SettingsPage-fqy7b9b8.js +1 -0
- package/dist/assets/{TailscalePage-pv47ZX2f.js → TailscalePage-Cf9_ENdO.js} +1 -1
- package/dist/assets/{index-C7qt99R_.js → index-Baw3PrqK.js} +52 -52
- package/dist/assets/{sw-register-DZ08pZIJ.js → sw-register-D-9XveNW.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/auto-namer.test.ts +4 -0
- package/server/claude-adapter.test.ts +111 -6
- package/server/claude-adapter.ts +106 -0
- package/server/linear-connections.test.ts +4 -0
- package/server/routes/settings-routes.ts +10 -0
- package/server/routes.test.ts +27 -0
- package/server/session-orchestrator.test.ts +3 -0
- package/server/settings-manager.test.ts +3 -0
- package/server/settings-manager.ts +14 -1
- package/server/ws-bridge-codex.test.ts +6 -0
- package/dist/assets/SettingsPage-CvYCqkm1.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-Baw3PrqK.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-Baw3PrqK.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-C9eyQq6F.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":"60133672097c53eb46cee2ded4a53250","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-D-9XveNW.js"},{"revision":null,"url":"assets/index-C9eyQq6F.css"},{"revision":null,"url":"assets/index-Baw3PrqK.js"},{"revision":null,"url":"assets/TailscalePage-Cf9_ENdO.js"},{"revision":null,"url":"assets/SettingsPage-fqy7b9b8.js"},{"revision":null,"url":"assets/SandboxManager-Ch0-1iE8.js"},{"revision":null,"url":"assets/RunsPage-ib31kftd.js"},{"revision":null,"url":"assets/PromptsPage-Bt2idDR8.js"},{"revision":null,"url":"assets/Playground-BheM7iin.js"},{"revision":null,"url":"assets/LinearSettingsPage-C-Gm_aeW.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-v4LYNLyW.js"},{"revision":null,"url":"assets/IntegrationsPage-zBdnuEDO.js"},{"revision":null,"url":"assets/DashboardPage-_gU5oXmV.js"},{"revision":null,"url":"assets/CronManager-CKvnO3df.js"},{"revision":null,"url":"assets/AgentsPage-Dc23pQBM.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
|
package/package.json
CHANGED
|
@@ -43,6 +43,7 @@ beforeEach(() => {
|
|
|
43
43
|
dockerAutoUpdate: false,
|
|
44
44
|
proactiveKeepaliveEnabled: true,
|
|
45
45
|
wedgeKillEnabled: true,
|
|
46
|
+
silenceProbeEnabled: true,
|
|
46
47
|
updatedAt: 0,
|
|
47
48
|
});
|
|
48
49
|
});
|
|
@@ -92,6 +93,7 @@ describe("generateSessionTitle", () => {
|
|
|
92
93
|
dockerAutoUpdate: false,
|
|
93
94
|
proactiveKeepaliveEnabled: true,
|
|
94
95
|
wedgeKillEnabled: true,
|
|
96
|
+
silenceProbeEnabled: true,
|
|
95
97
|
updatedAt: 0,
|
|
96
98
|
});
|
|
97
99
|
|
|
@@ -148,6 +150,7 @@ describe("generateSessionTitle", () => {
|
|
|
148
150
|
dockerAutoUpdate: false,
|
|
149
151
|
proactiveKeepaliveEnabled: true,
|
|
150
152
|
wedgeKillEnabled: true,
|
|
153
|
+
silenceProbeEnabled: true,
|
|
151
154
|
updatedAt: 0,
|
|
152
155
|
});
|
|
153
156
|
mockFetch.mockResolvedValueOnce({
|
|
@@ -233,6 +236,7 @@ describe("generateSessionTitle", () => {
|
|
|
233
236
|
dockerAutoUpdate: false,
|
|
234
237
|
proactiveKeepaliveEnabled: true,
|
|
235
238
|
wedgeKillEnabled: true,
|
|
239
|
+
silenceProbeEnabled: true,
|
|
236
240
|
updatedAt: 0,
|
|
237
241
|
});
|
|
238
242
|
mockFetch.mockResolvedValueOnce({
|
|
@@ -33,13 +33,18 @@ vi.mock("./proc-diagnostics.js", async (importOriginal) => ({
|
|
|
33
33
|
countDescendants: mockCountDescendants,
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
|
+
// Settings are stubbed so the wedge-kill and silence-probe switches can be
|
|
37
|
+
// driven per test. Both default to enabled, matching production.
|
|
38
|
+
const mockSettings = vi.hoisted(() => ({
|
|
39
|
+
aiValidationEnabled: false,
|
|
40
|
+
aiValidationAutoApprove: false,
|
|
41
|
+
aiValidationAutoDeny: false,
|
|
42
|
+
anthropicApiKey: "",
|
|
43
|
+
wedgeKillEnabled: true,
|
|
44
|
+
silenceProbeEnabled: true,
|
|
45
|
+
}));
|
|
36
46
|
vi.mock("./settings-manager.js", () => ({
|
|
37
|
-
getSettings: () =>
|
|
38
|
-
aiValidationEnabled: false,
|
|
39
|
-
aiValidationAutoApprove: false,
|
|
40
|
-
aiValidationAutoDeny: false,
|
|
41
|
-
anthropicApiKey: "",
|
|
42
|
-
}),
|
|
47
|
+
getSettings: () => mockSettings,
|
|
43
48
|
DEFAULT_ANTHROPIC_MODEL: "claude-sonnet-4-6",
|
|
44
49
|
}));
|
|
45
50
|
|
|
@@ -1765,3 +1770,103 @@ describe("stdio transport disconnect propagation", () => {
|
|
|
1765
1770
|
expect(disconnectCb).toHaveBeenCalledTimes(1);
|
|
1766
1771
|
});
|
|
1767
1772
|
});
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Silence probe — a failure mode none of the stdout-EOF machinery can see.
|
|
1776
|
+
*
|
|
1777
|
+
* Reported from production: the CLI stops emitting mid-`thinking_delta` while
|
|
1778
|
+
* its stdout stays OPEN and the process stays healthy (S/ep_poll, 12s CPU over
|
|
1779
|
+
* 13min), with upstream HTTPS sockets ESTABLISHED and idle. Because nothing
|
|
1780
|
+
* ever closes the stream, no wedge warning, kill, relaunch or error fires, and
|
|
1781
|
+
* the session hangs indefinitely showing "Still working…".
|
|
1782
|
+
*
|
|
1783
|
+
* A stalled CLI also stops answering control requests, which is the signal the
|
|
1784
|
+
* probe keys on.
|
|
1785
|
+
*/
|
|
1786
|
+
describe("stdio silence probe", () => {
|
|
1787
|
+
let adapter: ClaudeAdapter;
|
|
1788
|
+
let disconnectCb: ReturnType<typeof vi.fn>;
|
|
1789
|
+
|
|
1790
|
+
beforeEach(() => {
|
|
1791
|
+
adapter = new ClaudeAdapter("silence-session");
|
|
1792
|
+
disconnectCb = vi.fn();
|
|
1793
|
+
adapter.onDisconnect(disconnectCb as unknown as () => void);
|
|
1794
|
+
mockSettings.silenceProbeEnabled = true;
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
afterEach(() => {
|
|
1798
|
+
mockSettings.silenceProbeEnabled = true;
|
|
1799
|
+
vi.useRealTimers();
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
it("declares the transport dead when a silent CLI never answers the probe", async () => {
|
|
1803
|
+
vi.useFakeTimers();
|
|
1804
|
+
const { proc } = createMockProc();
|
|
1805
|
+
adapter.attachStdio(proc);
|
|
1806
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1807
|
+
|
|
1808
|
+
// Silence short of the probe threshold: nothing happens yet.
|
|
1809
|
+
await vi.advanceTimersByTimeAsync(110_000);
|
|
1810
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1811
|
+
|
|
1812
|
+
// Past the threshold the probe is sent, but the CLI never replies.
|
|
1813
|
+
await vi.advanceTimersByTimeAsync(20_000);
|
|
1814
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1815
|
+
|
|
1816
|
+
// Past the probe timeout the transport is treated as dead, handing off to
|
|
1817
|
+
// the relaunch path that replays the in-flight turn.
|
|
1818
|
+
await vi.advanceTimersByTimeAsync(65_000);
|
|
1819
|
+
expect(disconnectCb).toHaveBeenCalledTimes(1);
|
|
1820
|
+
expect(adapter.isConnected()).toBe(false);
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
it("does NOT fire while the CLI keeps emitting", async () => {
|
|
1824
|
+
// The critical false-positive guard. A working session must never be torn
|
|
1825
|
+
// down — every previous timer in this file was too aggressive and killed
|
|
1826
|
+
// healthy processes.
|
|
1827
|
+
vi.useFakeTimers();
|
|
1828
|
+
const { proc, pushStdout } = createMockProc();
|
|
1829
|
+
adapter.attachStdio(proc);
|
|
1830
|
+
|
|
1831
|
+
for (let i = 0; i < 10; i++) {
|
|
1832
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
1833
|
+
pushStdout(
|
|
1834
|
+
JSON.stringify({ type: "keep_alive", uuid: `ka-${i}`, session_id: "s" }) + "\n",
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
1838
|
+
|
|
1839
|
+
// 10+ minutes elapsed, but the stream kept proving life.
|
|
1840
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1841
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
it("treats a late reply as proof of life and cancels the pending probe", async () => {
|
|
1845
|
+
// The CLI answers *after* the probe goes out but before the timeout. That
|
|
1846
|
+
// is a slow CLI, not a dead one, and must not be torn down.
|
|
1847
|
+
vi.useFakeTimers();
|
|
1848
|
+
const { proc, pushStdout } = createMockProc();
|
|
1849
|
+
adapter.attachStdio(proc);
|
|
1850
|
+
|
|
1851
|
+
await vi.advanceTimersByTimeAsync(130_000); // probe sent
|
|
1852
|
+
pushStdout(
|
|
1853
|
+
JSON.stringify({ type: "keep_alive", uuid: "late", session_id: "s" }) + "\n",
|
|
1854
|
+
);
|
|
1855
|
+
await vi.advanceTimersByTimeAsync(65_000); // past what would have been the timeout
|
|
1856
|
+
|
|
1857
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1858
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
it("does nothing when the silence probe is disabled", async () => {
|
|
1862
|
+
vi.useFakeTimers();
|
|
1863
|
+
mockSettings.silenceProbeEnabled = false;
|
|
1864
|
+
const { proc } = createMockProc();
|
|
1865
|
+
adapter.attachStdio(proc);
|
|
1866
|
+
|
|
1867
|
+
await vi.advanceTimersByTimeAsync(600_000);
|
|
1868
|
+
|
|
1869
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1870
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1871
|
+
});
|
|
1872
|
+
});
|
package/server/claude-adapter.ts
CHANGED
|
@@ -102,6 +102,33 @@ const SIGKILL_ESCALATION_MS = Number(process.env.COMPANION_SIGKILL_ESCALATION_MS
|
|
|
102
102
|
*/
|
|
103
103
|
const TEARDOWN_MAX_MS = Number(process.env.COMPANION_TEARDOWN_MAX_MS) || 660_000;
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Silence-probe thresholds.
|
|
107
|
+
*
|
|
108
|
+
* A separate failure mode from the wedge: the CLI's stdout stays **open** and
|
|
109
|
+
* the process stays healthy (S/ep_poll, low CPU), but it stops emitting
|
|
110
|
+
* entirely — observed mid-`thinking_delta`, with its upstream HTTPS sockets
|
|
111
|
+
* ESTABLISHED and idle (Recv-Q/Send-Q 0) and no client-side timeout ever
|
|
112
|
+
* firing. Because stdout never closes, none of the stdout-EOF machinery runs:
|
|
113
|
+
* no warning, no kill, no relaunch. The session hangs forever and the UI shows
|
|
114
|
+
* "Still working…" indefinitely.
|
|
115
|
+
*
|
|
116
|
+
* A stalled CLI also stops answering control requests, while a healthy one
|
|
117
|
+
* replies promptly. So after SILENCE_PROBE_AFTER_MS of total silence we send a
|
|
118
|
+
* cheap control_request; if nothing at all comes back within
|
|
119
|
+
* SILENCE_PROBE_TIMEOUT_MS we treat the transport as dead and hand off to the
|
|
120
|
+
* existing relaunch path, which replays the in-flight turn rather than losing
|
|
121
|
+
* it.
|
|
122
|
+
*
|
|
123
|
+
* Thresholds are deliberately generous (~3 minutes end to end). Every previous
|
|
124
|
+
* timer in this file has turned out to be too aggressive and killed healthy
|
|
125
|
+
* processes, and the cost of being wrong here is the same: a needless restart.
|
|
126
|
+
* Any inbound frame — including `keep_alive` — counts as proof of life.
|
|
127
|
+
*/
|
|
128
|
+
const SILENCE_PROBE_AFTER_MS = Number(process.env.COMPANION_SILENCE_PROBE_AFTER_MS) || 120_000;
|
|
129
|
+
const SILENCE_PROBE_TIMEOUT_MS = Number(process.env.COMPANION_SILENCE_PROBE_TIMEOUT_MS) || 60_000;
|
|
130
|
+
const SILENCE_CHECK_INTERVAL_MS = Number(process.env.COMPANION_SILENCE_CHECK_INTERVAL_MS) || 15_000;
|
|
131
|
+
|
|
105
132
|
/** How often to re-check the descendant count while waiting for teardown. */
|
|
106
133
|
const TEARDOWN_POLL_MS = Number(process.env.COMPANION_TEARDOWN_POLL_MS) || 500;
|
|
107
134
|
|
|
@@ -141,6 +168,11 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
141
168
|
* (`keep_alive`, `system` status/keepalive) does not reset it; any
|
|
142
169
|
* substantive turn activity does. */
|
|
143
170
|
private lastInboundWasResult = false;
|
|
171
|
+
/** Timestamp of the last inbound frame of any kind, for the silence probe. */
|
|
172
|
+
private lastInboundAt = Date.now();
|
|
173
|
+
/** When the outstanding silence probe was sent, or null if none is in flight. */
|
|
174
|
+
private probeSentAt: number | null = null;
|
|
175
|
+
private silenceTimer: ReturnType<typeof setInterval> | null = null;
|
|
144
176
|
|
|
145
177
|
// Callbacks registered by the bridge via on*() methods
|
|
146
178
|
private browserMessageCb: ((msg: BrowserIncomingMessage) => void) | null = null;
|
|
@@ -273,6 +305,10 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
273
305
|
const stdout = proc.stdout as ReadableStream<Uint8Array>;
|
|
274
306
|
void this.readStdioStdout(stdout);
|
|
275
307
|
|
|
308
|
+
// Watch for the CLI going silent while stdout stays open — invisible to the
|
|
309
|
+
// stdout-EOF path, which only runs when the stream actually closes.
|
|
310
|
+
this.startSilenceProbe();
|
|
311
|
+
|
|
276
312
|
// Flush anything queued before the transport attached (applies the lazy
|
|
277
313
|
// `initialize` handshake before the first real message).
|
|
278
314
|
if (this.pendingMessages.length > 0) {
|
|
@@ -308,6 +344,7 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
308
344
|
private notifyStdioDisconnect(): void {
|
|
309
345
|
this.stdioConnected = false;
|
|
310
346
|
this.stdioWriter = null;
|
|
347
|
+
this.stopSilenceProbe();
|
|
311
348
|
if (this.disconnectFired) return;
|
|
312
349
|
this.disconnectFired = true;
|
|
313
350
|
this.disconnectCb?.();
|
|
@@ -440,6 +477,68 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
440
477
|
/** Why the last teardown wait ended, for the kill warning. */
|
|
441
478
|
private lastTeardownOutcome: string | undefined;
|
|
442
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Start the silence probe for the stdio transport.
|
|
482
|
+
*
|
|
483
|
+
* Detects a CLI that has gone silent with its stdout still **open** — the one
|
|
484
|
+
* failure mode none of the stdout-EOF machinery can see, because nothing ever
|
|
485
|
+
* closes. Left alone the session hangs indefinitely with no error surfaced.
|
|
486
|
+
*/
|
|
487
|
+
private startSilenceProbe(): void {
|
|
488
|
+
this.stopSilenceProbe();
|
|
489
|
+
this.lastInboundAt = Date.now();
|
|
490
|
+
this.probeSentAt = null;
|
|
491
|
+
this.silenceTimer = setInterval(() => this.checkSilence(), SILENCE_CHECK_INTERVAL_MS);
|
|
492
|
+
// Never hold the process open for a diagnostic timer.
|
|
493
|
+
(this.silenceTimer as unknown as { unref?: () => void }).unref?.();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private stopSilenceProbe(): void {
|
|
497
|
+
if (this.silenceTimer) {
|
|
498
|
+
clearInterval(this.silenceTimer);
|
|
499
|
+
this.silenceTimer = null;
|
|
500
|
+
}
|
|
501
|
+
this.probeSentAt = null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private checkSilence(): void {
|
|
505
|
+
if (this.transportKind !== "stdio" || !this.stdioConnected) return;
|
|
506
|
+
if (getSettings().silenceProbeEnabled === false) return;
|
|
507
|
+
|
|
508
|
+
const now = Date.now();
|
|
509
|
+
|
|
510
|
+
// An unanswered probe is the actual failure signal: a healthy CLI replies
|
|
511
|
+
// to control requests promptly even while working.
|
|
512
|
+
if (this.probeSentAt !== null) {
|
|
513
|
+
if (now - this.probeSentAt >= SILENCE_PROBE_TIMEOUT_MS) {
|
|
514
|
+
log.warn("claude-adapter", "CLI silent with stdout open and probe unanswered; treating transport as dead", {
|
|
515
|
+
sessionId: this.sessionId,
|
|
516
|
+
pid: this.stdioProc?.pid,
|
|
517
|
+
silentForMs: now - this.lastInboundAt,
|
|
518
|
+
probeUnansweredForMs: now - this.probeSentAt,
|
|
519
|
+
proc: captureProcState(this.stdioProc?.pid),
|
|
520
|
+
});
|
|
521
|
+
this.stopSilenceProbe();
|
|
522
|
+
// Hand off to the existing recovery path, which relaunches and replays
|
|
523
|
+
// the in-flight turn rather than losing it.
|
|
524
|
+
this.notifyStdioDisconnect();
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (now - this.lastInboundAt >= SILENCE_PROBE_AFTER_MS) {
|
|
530
|
+
this.probeSentAt = now;
|
|
531
|
+
log.info("claude-adapter", "CLI silent; sending liveness probe", {
|
|
532
|
+
sessionId: this.sessionId,
|
|
533
|
+
pid: this.stdioProc?.pid,
|
|
534
|
+
silentForMs: now - this.lastInboundAt,
|
|
535
|
+
});
|
|
536
|
+
// mcp_status is cheap and side-effect free. We do not care about the
|
|
537
|
+
// payload — any inbound frame clears probeSentAt in routeCLIMessage.
|
|
538
|
+
this.sendControlRequest({ subtype: "mcp_status" });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
443
542
|
/**
|
|
444
543
|
* Wait for a process to exit after its stdout transport has died.
|
|
445
544
|
*
|
|
@@ -955,6 +1054,13 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
955
1054
|
// -- CLI message routing (NDJSON -> BrowserIncomingMessage) -----------------
|
|
956
1055
|
|
|
957
1056
|
private routeCLIMessage(msg: CLIMessage): void {
|
|
1057
|
+
// Any frame at all — keep_alive included — proves the transport is alive
|
|
1058
|
+
// and clears any outstanding silence probe. This is deliberately broader
|
|
1059
|
+
// than the activity tracking below: `keep_alive` is not *work*, but it is
|
|
1060
|
+
// proof the CLI is still answering, which is all the probe asks.
|
|
1061
|
+
this.lastInboundAt = Date.now();
|
|
1062
|
+
this.probeSentAt = null;
|
|
1063
|
+
|
|
958
1064
|
// Track activity for idle detection (skip keepalives -- they don't indicate real work)
|
|
959
1065
|
if (msg.type !== "keep_alive") {
|
|
960
1066
|
this.onActivityUpdate?.();
|
|
@@ -69,6 +69,7 @@ beforeEach(() => {
|
|
|
69
69
|
dockerAutoUpdate: false,
|
|
70
70
|
proactiveKeepaliveEnabled: true,
|
|
71
71
|
wedgeKillEnabled: true,
|
|
72
|
+
silenceProbeEnabled: true,
|
|
72
73
|
updatedAt: 0,
|
|
73
74
|
});
|
|
74
75
|
});
|
|
@@ -206,6 +207,7 @@ describe("linear-connections", () => {
|
|
|
206
207
|
dockerAutoUpdate: false,
|
|
207
208
|
proactiveKeepaliveEnabled: true,
|
|
208
209
|
wedgeKillEnabled: true,
|
|
210
|
+
silenceProbeEnabled: true,
|
|
209
211
|
updatedAt: 0,
|
|
210
212
|
});
|
|
211
213
|
// Need to reset so migration runs with updated mock
|
|
@@ -253,6 +255,7 @@ describe("linear-connections", () => {
|
|
|
253
255
|
dockerAutoUpdate: false,
|
|
254
256
|
proactiveKeepaliveEnabled: true,
|
|
255
257
|
wedgeKillEnabled: true,
|
|
258
|
+
silenceProbeEnabled: true,
|
|
256
259
|
updatedAt: 0,
|
|
257
260
|
});
|
|
258
261
|
_resetForTest(join(tempDir, "linear-connections-migrate.json"));
|
|
@@ -319,6 +322,7 @@ describe("linear-connections", () => {
|
|
|
319
322
|
dockerAutoUpdate: false,
|
|
320
323
|
proactiveKeepaliveEnabled: true,
|
|
321
324
|
wedgeKillEnabled: true,
|
|
325
|
+
silenceProbeEnabled: true,
|
|
322
326
|
updatedAt: 0,
|
|
323
327
|
});
|
|
324
328
|
|
|
@@ -35,6 +35,7 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
35
35
|
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
36
36
|
proactiveKeepaliveEnabled: settings.proactiveKeepaliveEnabled,
|
|
37
37
|
wedgeKillEnabled: settings.wedgeKillEnabled,
|
|
38
|
+
silenceProbeEnabled: settings.silenceProbeEnabled,
|
|
38
39
|
cliBridgeMode: settings.cliBridgeMode,
|
|
39
40
|
});
|
|
40
41
|
});
|
|
@@ -136,6 +137,9 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
136
137
|
if (body.wedgeKillEnabled !== undefined && typeof body.wedgeKillEnabled !== "boolean") {
|
|
137
138
|
return c.json({ error: "wedgeKillEnabled must be a boolean" }, 400);
|
|
138
139
|
}
|
|
140
|
+
if (body.silenceProbeEnabled !== undefined && typeof body.silenceProbeEnabled !== "boolean") {
|
|
141
|
+
return c.json({ error: "silenceProbeEnabled must be a boolean" }, 400);
|
|
142
|
+
}
|
|
139
143
|
if (body.cliBridgeMode !== undefined && body.cliBridgeMode !== "loopback" && body.cliBridgeMode !== "jsonHandoff") {
|
|
140
144
|
return c.json({ error: "cliBridgeMode must be 'loopback' or 'jsonHandoff'" }, 400);
|
|
141
145
|
}
|
|
@@ -157,6 +161,7 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
157
161
|
|| body.dockerAutoUpdate !== undefined
|
|
158
162
|
|| body.proactiveKeepaliveEnabled !== undefined
|
|
159
163
|
|| body.wedgeKillEnabled !== undefined
|
|
164
|
+
|| body.silenceProbeEnabled !== undefined
|
|
160
165
|
|| body.cliBridgeMode !== undefined;
|
|
161
166
|
if (!hasAnyField) {
|
|
162
167
|
return c.json({ error: "At least one settings field is required" }, 400);
|
|
@@ -275,6 +280,10 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
275
280
|
typeof body.wedgeKillEnabled === "boolean"
|
|
276
281
|
? body.wedgeKillEnabled
|
|
277
282
|
: undefined,
|
|
283
|
+
silenceProbeEnabled:
|
|
284
|
+
typeof body.silenceProbeEnabled === "boolean"
|
|
285
|
+
? body.silenceProbeEnabled
|
|
286
|
+
: undefined,
|
|
278
287
|
cliBridgeMode:
|
|
279
288
|
body.cliBridgeMode === "loopback" || body.cliBridgeMode === "jsonHandoff"
|
|
280
289
|
? (body.cliBridgeMode as CliBridgeMode)
|
|
@@ -309,6 +318,7 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
309
318
|
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
310
319
|
proactiveKeepaliveEnabled: settings.proactiveKeepaliveEnabled,
|
|
311
320
|
wedgeKillEnabled: settings.wedgeKillEnabled,
|
|
321
|
+
silenceProbeEnabled: settings.silenceProbeEnabled,
|
|
312
322
|
cliBridgeMode: settings.cliBridgeMode,
|
|
313
323
|
});
|
|
314
324
|
});
|