@hellcoder/companion 0.105.3 → 0.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/assets/{AgentsPage-Chj3i8hp.js → AgentsPage-DbvkUXVp.js} +5 -5
  2. package/dist/assets/{CronManager-CnSV7pph.js → CronManager-BLIoyqgi.js} +1 -1
  3. package/dist/assets/{IntegrationsPage-D7He8DJZ.js → IntegrationsPage-DIG0VC9S.js} +1 -1
  4. package/dist/assets/{LinearOAuthSettingsPage-BhbSx1L1.js → LinearOAuthSettingsPage-VDcZL-vG.js} +1 -1
  5. package/dist/assets/{LinearSettingsPage-xYU0CnhC.js → LinearSettingsPage-N9lB7uAD.js} +1 -1
  6. package/dist/assets/{Playground-ZMOfN3kG.js → Playground-egZO3vjk.js} +1 -1
  7. package/dist/assets/{PromptsPage-C-YET0qF.js → PromptsPage-ByCRKIIK.js} +1 -1
  8. package/dist/assets/{RunsPage-D8p_NPsc.js → RunsPage-DRLiiAa4.js} +1 -1
  9. package/dist/assets/{SandboxManager-DNct4A2p.js → SandboxManager-D0UrXCUj.js} +1 -1
  10. package/dist/assets/SettingsPage-BESpvcQy.js +1 -0
  11. package/dist/assets/{TailscalePage-CVXN6STS.js → TailscalePage-hiU_mAeJ.js} +1 -1
  12. package/dist/assets/{index-B_8Ih1pQ.js → index-Ct0BP0Nf.js} +49 -49
  13. package/dist/assets/{sw-register-Gj6h8GCi.js → sw-register-DqHi_6mH.js} +1 -1
  14. package/dist/index.html +1 -1
  15. package/dist/sw.js +1 -1
  16. package/package.json +1 -1
  17. package/server/agent-executor.test.ts +99 -2
  18. package/server/agent-executor.ts +34 -4
  19. package/server/agent-types.ts +10 -0
  20. package/server/auto-namer.test.ts +4 -0
  21. package/server/linear-connections.test.ts +4 -0
  22. package/server/routes/agent-routes.ts +2 -1
  23. package/server/routes/settings-routes.ts +10 -0
  24. package/server/routes.test.ts +52 -0
  25. package/server/session-orchestrator.test.ts +44 -1
  26. package/server/session-orchestrator.ts +14 -0
  27. package/server/settings-manager.test.ts +3 -0
  28. package/server/settings-manager.ts +10 -1
  29. package/server/ws-bridge-codex.test.ts +6 -0
  30. package/dist/assets/SettingsPage-Bc33muUi.js +0 -1
@@ -1 +1 @@
1
- import{_ as u}from"./index-B_8Ih1pQ.js";function w(r={}){const{immediate:i=!1,onNeedReload:d,onNeedRefresh:_,onOfflineReady:s,onRegistered:n,onRegisteredSW:o,onRegisterError:t}=r;let a,c;const l=async(e=!0)=>{await c};async function f(){if("serviceWorker"in navigator){if(a=await u(async()=>{const{Workbox:e}=await import("./workbox-window.prod.es5-BBnX5xw4.js");return{Workbox:e}},[]).then(({Workbox:e})=>new e("/sw.js",{scope:"/",type:"classic"})).catch(e=>{t==null||t(e)}),!a)return;a.addEventListener("activated",e=>{(e.isUpdate||e.isExternal)&&(d?d():window.location.reload())}),a.addEventListener("installed",e=>{e.isUpdate||s==null||s()}),a.register({immediate:i}).then(e=>{o?o("/sw.js",e):n==null||n(e)}).catch(e=>{t==null||t(e)})}}return c=f(),l}w({onRegisteredSW(r,i){i&&setInterval(()=>{i.update()},3600*1e3)},onOfflineReady(){console.log("[SW] Offline-ready: all assets precached")}});
1
+ import{_ as u}from"./index-Ct0BP0Nf.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-B_8Ih1pQ.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-Ct0BP0Nf.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-D4AeOLki.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":"b2f5ebafc5fc24aa803f6ac80a769ee6","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-Gj6h8GCi.js"},{"revision":null,"url":"assets/index-D4AeOLki.css"},{"revision":null,"url":"assets/index-B_8Ih1pQ.js"},{"revision":null,"url":"assets/TailscalePage-CVXN6STS.js"},{"revision":null,"url":"assets/SettingsPage-Bc33muUi.js"},{"revision":null,"url":"assets/SandboxManager-DNct4A2p.js"},{"revision":null,"url":"assets/RunsPage-D8p_NPsc.js"},{"revision":null,"url":"assets/PromptsPage-C-YET0qF.js"},{"revision":null,"url":"assets/Playground-ZMOfN3kG.js"},{"revision":null,"url":"assets/LinearSettingsPage-xYU0CnhC.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-BhbSx1L1.js"},{"revision":null,"url":"assets/IntegrationsPage-D7He8DJZ.js"},{"revision":null,"url":"assets/CronManager-CnSV7pph.js"},{"revision":null,"url":"assets/AgentsPage-Chj3i8hp.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
2
+ This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"2d7031149d01133258a09051c022c4c1","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-DqHi_6mH.js"},{"revision":null,"url":"assets/index-D4AeOLki.css"},{"revision":null,"url":"assets/index-Ct0BP0Nf.js"},{"revision":null,"url":"assets/TailscalePage-hiU_mAeJ.js"},{"revision":null,"url":"assets/SettingsPage-BESpvcQy.js"},{"revision":null,"url":"assets/SandboxManager-D0UrXCUj.js"},{"revision":null,"url":"assets/RunsPage-DRLiiAa4.js"},{"revision":null,"url":"assets/PromptsPage-ByCRKIIK.js"},{"revision":null,"url":"assets/Playground-egZO3vjk.js"},{"revision":null,"url":"assets/LinearSettingsPage-N9lB7uAD.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-VDcZL-vG.js"},{"revision":null,"url":"assets/IntegrationsPage-DIG0VC9S.js"},{"revision":null,"url":"assets/CronManager-BLIoyqgi.js"},{"revision":null,"url":"assets/AgentsPage-DbvkUXVp.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.105.3",
3
+ "version": "0.106.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",
@@ -438,10 +438,11 @@ describe("AgentExecutor", () => {
438
438
  expect(launcher.launch).toHaveBeenCalledOnce();
439
439
  });
440
440
 
441
- it("skips when previous execution is still running (overlap prevention)", async () => {
442
- // Simulate an agent whose previous session is still alive
441
+ it("skips when previous execution is still open and restartMode is 'skip'", async () => {
442
+ // Simulate an agent (restartMode "skip") whose previous session is still alive
443
443
  const agent = makeAgent({
444
444
  id: "overlapping",
445
+ restartMode: "skip",
445
446
  lastSessionId: "still-running-session",
446
447
  });
447
448
  mockAgentStore.getAgent.mockReturnValue(agent);
@@ -723,6 +724,102 @@ describe("AgentExecutor", () => {
723
724
  });
724
725
  });
725
726
 
727
+ // =========================================================================
728
+ // restartMode — what happens when a previous run's session is still open
729
+ // =========================================================================
730
+ describe("restartMode (leftover session handling)", () => {
731
+ it("default 'terminate': archives the leftover session, then launches a fresh one", async () => {
732
+ // Regression test for the core bug: a scheduled agent's session stays alive
733
+ // after its turn finishes, so isAlive() is permanently true. With the default
734
+ // terminate mode we archive the leftover and start a new run instead of
735
+ // blocking forever.
736
+ const archiveSpy = vi.fn<(id: string) => Promise<void>>().mockResolvedValue(undefined);
737
+ executor.setArchivePreviousSession(archiveSpy);
738
+
739
+ const agent = makeAgent({
740
+ id: "terminate-agent",
741
+ // no restartMode set -> defaults to "terminate"
742
+ lastSessionId: "leftover-session",
743
+ });
744
+ mockAgentStore.getAgent.mockReturnValue(agent);
745
+ launcher.isAlive.mockReturnValue(true);
746
+
747
+ const result = await executor.executeAgent("terminate-agent", undefined, { triggerType: "schedule" });
748
+
749
+ // The leftover session should have been archived (intentional kill)...
750
+ expect(archiveSpy).toHaveBeenCalledWith("leftover-session");
751
+ // ...and a new session launched anyway.
752
+ expect(launcher.launch).toHaveBeenCalledOnce();
753
+ expect(result).toBeDefined();
754
+ expect(result!.sessionId).toBe("session-123");
755
+ });
756
+
757
+ it("does not archive when no previous session is alive", async () => {
758
+ const archiveSpy = vi.fn<(id: string) => Promise<void>>().mockResolvedValue(undefined);
759
+ executor.setArchivePreviousSession(archiveSpy);
760
+
761
+ const agent = makeAgent({ id: "fresh-agent", lastSessionId: "old-session" });
762
+ mockAgentStore.getAgent.mockReturnValue(agent);
763
+ // Previous session already exited.
764
+ launcher.isAlive.mockReturnValue(false);
765
+
766
+ await executor.executeAgent("fresh-agent");
767
+
768
+ expect(archiveSpy).not.toHaveBeenCalled();
769
+ expect(launcher.launch).toHaveBeenCalledOnce();
770
+ });
771
+
772
+ it("terminate mode still launches even if archiving the leftover fails", async () => {
773
+ // A failed archive should be logged but must not block the new run.
774
+ const archiveSpy = vi.fn<(id: string) => Promise<void>>().mockRejectedValue(new Error("kill failed"));
775
+ executor.setArchivePreviousSession(archiveSpy);
776
+
777
+ const agent = makeAgent({ id: "resilient-agent", lastSessionId: "leftover-session" });
778
+ mockAgentStore.getAgent.mockReturnValue(agent);
779
+ launcher.isAlive.mockReturnValue(true);
780
+
781
+ const result = await executor.executeAgent("resilient-agent");
782
+
783
+ expect(archiveSpy).toHaveBeenCalledWith("leftover-session");
784
+ expect(launcher.launch).toHaveBeenCalledOnce();
785
+ expect(result).toBeDefined();
786
+ });
787
+
788
+ it("terminate mode without a wired archive callback warns but still launches", async () => {
789
+ // No setArchivePreviousSession() called -> callback undefined.
790
+ const agent = makeAgent({ id: "no-callback-agent", lastSessionId: "leftover-session" });
791
+ mockAgentStore.getAgent.mockReturnValue(agent);
792
+ launcher.isAlive.mockReturnValue(true);
793
+
794
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
795
+
796
+ const result = await executor.executeAgent("no-callback-agent");
797
+
798
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("no archive callback wired"));
799
+ // Still launches a fresh session rather than getting stuck.
800
+ expect(launcher.launch).toHaveBeenCalledOnce();
801
+ expect(result).toBeDefined();
802
+
803
+ warnSpy.mockRestore();
804
+ });
805
+
806
+ it("skip mode with force=true bypasses the skip and launches", async () => {
807
+ // Manual/forced runs should never be blocked by the skip guard.
808
+ const agent = makeAgent({
809
+ id: "skip-force-agent",
810
+ restartMode: "skip",
811
+ lastSessionId: "leftover-session",
812
+ });
813
+ mockAgentStore.getAgent.mockReturnValue(agent);
814
+ launcher.isAlive.mockReturnValue(true);
815
+
816
+ const result = await executor.executeAgent("skip-force-agent", undefined, { force: true });
817
+
818
+ expect(launcher.launch).toHaveBeenCalledOnce();
819
+ expect(result).toBeDefined();
820
+ });
821
+ });
822
+
726
823
  // =========================================================================
727
824
  // waitForCLIConnection (tested indirectly via executeAgent)
728
825
  // =========================================================================
@@ -34,11 +34,24 @@ export class AgentExecutor {
34
34
  /** Persistent execution store (JSONL on disk) */
35
35
  private executionStore = new ExecutionStore();
36
36
 
37
+ /**
38
+ * Callback used to intentionally terminate a leftover session before starting
39
+ * a new run (restartMode "terminate"). Wired by the orchestrator to its
40
+ * archiveSession() so the kill is registered as intentional — otherwise the
41
+ * keepalive relaunch would immediately respawn the process.
42
+ */
43
+ private archivePreviousSession?: (sessionId: string) => Promise<void>;
44
+
37
45
  constructor(launcher: CliLauncher, wsBridge: WsBridge) {
38
46
  this.launcher = launcher;
39
47
  this.wsBridge = wsBridge;
40
48
  }
41
49
 
50
+ /** Wire the callback used to archive a leftover session (see field docs). */
51
+ setArchivePreviousSession(fn: (sessionId: string) => Promise<void>): void {
52
+ this.archivePreviousSession = fn;
53
+ }
54
+
42
55
  /** Start all enabled agents with schedule triggers from disk. Called once at server startup. */
43
56
  startAll(): void {
44
57
  const agents = agentStore.listAgents();
@@ -123,10 +136,27 @@ export class AgentExecutor {
123
136
  if (!agent) return;
124
137
  if (!agent.enabled && !opts?.force) return;
125
138
 
126
- // Overlap prevention: skip if previous execution is still running (unless forced)
127
- if (!opts?.force && agent.lastSessionId && this.launcher.isAlive(agent.lastSessionId)) {
128
- console.log(`[agent-executor] Skipping "${agent.name}" previous execution still running (${agent.lastSessionId})`);
129
- return;
139
+ // Overlap handling: a previous run's session may still be open. Because the
140
+ // CLI process stays alive after a turn finishes (it only exits on
141
+ // archive/kill), we can't distinguish "still working" from "idle but open"
142
+ // via liveness alone — so the agent's restartMode decides what to do.
143
+ if (agent.lastSessionId && this.launcher.isAlive(agent.lastSessionId)) {
144
+ const restartMode = agent.restartMode ?? "terminate";
145
+ if (restartMode === "terminate") {
146
+ // Terminate the leftover session (intentional kill) before starting fresh.
147
+ console.log(`[agent-executor] Terminating leftover session for "${agent.name}" before new run (${agent.lastSessionId})`);
148
+ if (this.archivePreviousSession) {
149
+ await this.archivePreviousSession(agent.lastSessionId).catch((err) => {
150
+ console.error(`[agent-executor] Failed to terminate leftover session ${agent.lastSessionId}:`, err);
151
+ });
152
+ } else {
153
+ console.warn(`[agent-executor] restartMode="terminate" but no archive callback wired; leftover session ${agent.lastSessionId} left running`);
154
+ }
155
+ } else if (!opts?.force) {
156
+ // restartMode "skip": don't start while the previous run is still open.
157
+ console.log(`[agent-executor] Skipping "${agent.name}" — previous execution still open (${agent.lastSessionId})`);
158
+ return;
159
+ }
130
160
  }
131
161
 
132
162
  const triggerType = opts?.triggerType || "manual";
@@ -37,6 +37,16 @@ export interface AgentConfig {
37
37
  allowedTools?: string[];
38
38
  /** Codex-specific: internet access */
39
39
  codexInternetAccess?: boolean;
40
+ /**
41
+ * What to do when a previous run's session is still open (alive) at the moment
42
+ * this agent is triggered again:
43
+ * - "terminate" (default): archive/kill the leftover session, then start fresh.
44
+ * - "skip": don't start a new run while the previous one is still open.
45
+ * Because the CLI process stays alive after a turn finishes (it only exits on
46
+ * archive/kill), "skip" would otherwise block every future scheduled run until
47
+ * the session is manually archived.
48
+ */
49
+ restartMode?: "skip" | "terminate";
40
50
 
41
51
  // ── Prompt ──
42
52
  /** Prompt template. Use {{input}} as placeholder for trigger-provided input */
@@ -37,6 +37,7 @@ beforeEach(() => {
37
37
  publicUrl: "",
38
38
  updateChannel: "stable",
39
39
  dockerAutoUpdate: false,
40
+ proactiveKeepaliveEnabled: true,
40
41
  updatedAt: 0,
41
42
  });
42
43
  });
@@ -80,6 +81,7 @@ describe("generateSessionTitle", () => {
80
81
  publicUrl: "",
81
82
  updateChannel: "stable",
82
83
  dockerAutoUpdate: false,
84
+ proactiveKeepaliveEnabled: true,
83
85
  updatedAt: 0,
84
86
  });
85
87
 
@@ -130,6 +132,7 @@ describe("generateSessionTitle", () => {
130
132
  publicUrl: "",
131
133
  updateChannel: "stable",
132
134
  dockerAutoUpdate: false,
135
+ proactiveKeepaliveEnabled: true,
133
136
  updatedAt: 0,
134
137
  });
135
138
  mockFetch.mockResolvedValueOnce({
@@ -209,6 +212,7 @@ describe("generateSessionTitle", () => {
209
212
  publicUrl: "",
210
213
  updateChannel: "stable",
211
214
  dockerAutoUpdate: false,
215
+ proactiveKeepaliveEnabled: true,
212
216
  updatedAt: 0,
213
217
  });
214
218
  mockFetch.mockResolvedValueOnce({
@@ -63,6 +63,7 @@ beforeEach(() => {
63
63
  publicUrl: "",
64
64
  updateChannel: "stable",
65
65
  dockerAutoUpdate: false,
66
+ proactiveKeepaliveEnabled: true,
66
67
  updatedAt: 0,
67
68
  });
68
69
  });
@@ -194,6 +195,7 @@ describe("linear-connections", () => {
194
195
  publicUrl: "",
195
196
  updateChannel: "stable",
196
197
  dockerAutoUpdate: false,
198
+ proactiveKeepaliveEnabled: true,
197
199
  updatedAt: 0,
198
200
  });
199
201
  // Need to reset so migration runs with updated mock
@@ -235,6 +237,7 @@ describe("linear-connections", () => {
235
237
  publicUrl: "",
236
238
  updateChannel: "stable",
237
239
  dockerAutoUpdate: false,
240
+ proactiveKeepaliveEnabled: true,
238
241
  updatedAt: 0,
239
242
  });
240
243
  _resetForTest(join(tempDir, "linear-connections-migrate.json"));
@@ -295,6 +298,7 @@ describe("linear-connections", () => {
295
298
  publicUrl: "",
296
299
  updateChannel: "stable",
297
300
  dockerAutoUpdate: false,
301
+ proactiveKeepaliveEnabled: true,
298
302
  updatedAt: 0,
299
303
  });
300
304
 
@@ -11,7 +11,7 @@ import { getOAuthConnection, createOAuthConnection } from "../linear-oauth-conne
11
11
  const EDITABLE_FIELDS = [
12
12
  "name", "description", "icon", "version",
13
13
  "backendType", "model", "permissionMode", "cwd",
14
- "envSlug", "env", "allowedTools", "codexInternetAccess",
14
+ "envSlug", "env", "allowedTools", "codexInternetAccess", "restartMode",
15
15
  "prompt", "mcpServers", "skills",
16
16
  "container", "branch", "createBranch", "useWorktree",
17
17
  "triggers", "enabled",
@@ -42,6 +42,7 @@ function buildCreateInput(
42
42
  env: body.env as Record<string, string> | undefined,
43
43
  allowedTools: body.allowedTools as string[] | undefined,
44
44
  codexInternetAccess: body.codexInternetAccess as boolean | undefined,
45
+ restartMode: body.restartMode as AgentConfig["restartMode"] | undefined,
45
46
  prompt: (body.prompt as string | undefined) || "",
46
47
  mcpServers: body.mcpServers as AgentConfig["mcpServers"] | undefined,
47
48
  skills: body.skills as string[] | undefined,
@@ -29,6 +29,7 @@ export function registerSettingsRoutes(api: Hono): void {
29
29
  publicUrl: settings.publicUrl,
30
30
  updateChannel: settings.updateChannel,
31
31
  dockerAutoUpdate: settings.dockerAutoUpdate,
32
+ proactiveKeepaliveEnabled: settings.proactiveKeepaliveEnabled,
32
33
  cliBridgeMode: settings.cliBridgeMode,
33
34
  });
34
35
  });
@@ -104,6 +105,9 @@ export function registerSettingsRoutes(api: Hono): void {
104
105
  if (body.dockerAutoUpdate !== undefined && typeof body.dockerAutoUpdate !== "boolean") {
105
106
  return c.json({ error: "dockerAutoUpdate must be a boolean" }, 400);
106
107
  }
108
+ if (body.proactiveKeepaliveEnabled !== undefined && typeof body.proactiveKeepaliveEnabled !== "boolean") {
109
+ return c.json({ error: "proactiveKeepaliveEnabled must be a boolean" }, 400);
110
+ }
107
111
  if (body.cliBridgeMode !== undefined && body.cliBridgeMode !== "loopback" && body.cliBridgeMode !== "jsonHandoff") {
108
112
  return c.json({ error: "cliBridgeMode must be 'loopback' or 'jsonHandoff'" }, 400);
109
113
  }
@@ -121,6 +125,7 @@ export function registerSettingsRoutes(api: Hono): void {
121
125
  || body.publicUrl !== undefined
122
126
  || body.updateChannel !== undefined
123
127
  || body.dockerAutoUpdate !== undefined
128
+ || body.proactiveKeepaliveEnabled !== undefined
124
129
  || body.cliBridgeMode !== undefined;
125
130
  if (!hasAnyField) {
126
131
  return c.json({ error: "At least one settings field is required" }, 400);
@@ -215,6 +220,10 @@ export function registerSettingsRoutes(api: Hono): void {
215
220
  typeof body.dockerAutoUpdate === "boolean"
216
221
  ? body.dockerAutoUpdate
217
222
  : undefined,
223
+ proactiveKeepaliveEnabled:
224
+ typeof body.proactiveKeepaliveEnabled === "boolean"
225
+ ? body.proactiveKeepaliveEnabled
226
+ : undefined,
218
227
  cliBridgeMode:
219
228
  body.cliBridgeMode === "loopback" || body.cliBridgeMode === "jsonHandoff"
220
229
  ? (body.cliBridgeMode as CliBridgeMode)
@@ -243,6 +252,7 @@ export function registerSettingsRoutes(api: Hono): void {
243
252
  publicUrl: settings.publicUrl,
244
253
  updateChannel: settings.updateChannel,
245
254
  dockerAutoUpdate: settings.dockerAutoUpdate,
255
+ proactiveKeepaliveEnabled: settings.proactiveKeepaliveEnabled,
246
256
  cliBridgeMode: settings.cliBridgeMode,
247
257
  });
248
258
  });
@@ -1149,6 +1149,7 @@ describe("GET /api/sessions/:id/archive-info", () => {
1149
1149
  publicUrl: "",
1150
1150
  updateChannel: "stable",
1151
1151
  dockerAutoUpdate: false,
1152
+ proactiveKeepaliveEnabled: true,
1152
1153
  updatedAt: 0,
1153
1154
  });
1154
1155
  const res = await app.request("/api/sessions/s1/archive-info", { method: "GET" });
@@ -1519,6 +1520,7 @@ describe("GET /api/settings", () => {
1519
1520
  publicUrl: "",
1520
1521
  updateChannel: "stable",
1521
1522
  dockerAutoUpdate: false,
1523
+ proactiveKeepaliveEnabled: true,
1522
1524
  updatedAt: 123,
1523
1525
  });
1524
1526
 
@@ -1547,6 +1549,7 @@ describe("GET /api/settings", () => {
1547
1549
  publicUrl: "",
1548
1550
  updateChannel: "stable",
1549
1551
  dockerAutoUpdate: false,
1552
+ proactiveKeepaliveEnabled: true,
1550
1553
  });
1551
1554
  });
1552
1555
 
@@ -1575,6 +1578,7 @@ describe("GET /api/settings", () => {
1575
1578
  publicUrl: "",
1576
1579
  updateChannel: "stable",
1577
1580
  dockerAutoUpdate: false,
1581
+ proactiveKeepaliveEnabled: true,
1578
1582
  updatedAt: 123,
1579
1583
  });
1580
1584
 
@@ -1603,6 +1607,7 @@ describe("GET /api/settings", () => {
1603
1607
  publicUrl: "",
1604
1608
  updateChannel: "stable",
1605
1609
  dockerAutoUpdate: false,
1610
+ proactiveKeepaliveEnabled: true,
1606
1611
  });
1607
1612
  });
1608
1613
 
@@ -1632,6 +1637,7 @@ describe("GET /api/settings", () => {
1632
1637
  publicUrl: "https://example.com",
1633
1638
  updateChannel: "stable",
1634
1639
  dockerAutoUpdate: false,
1640
+ proactiveKeepaliveEnabled: true,
1635
1641
  updatedAt: 100,
1636
1642
  });
1637
1643
 
@@ -1669,6 +1675,7 @@ describe("PUT /api/settings", () => {
1669
1675
  publicUrl: "",
1670
1676
  updateChannel: "stable",
1671
1677
  dockerAutoUpdate: false,
1678
+ proactiveKeepaliveEnabled: true,
1672
1679
  updatedAt: 456,
1673
1680
  });
1674
1681
 
@@ -1719,6 +1726,7 @@ describe("PUT /api/settings", () => {
1719
1726
  publicUrl: "",
1720
1727
  updateChannel: "stable",
1721
1728
  dockerAutoUpdate: false,
1729
+ proactiveKeepaliveEnabled: true,
1722
1730
  });
1723
1731
  });
1724
1732
 
@@ -1747,6 +1755,7 @@ describe("PUT /api/settings", () => {
1747
1755
  publicUrl: "",
1748
1756
  updateChannel: "stable",
1749
1757
  dockerAutoUpdate: false,
1758
+ proactiveKeepaliveEnabled: true,
1750
1759
  updatedAt: 789,
1751
1760
  });
1752
1761
 
@@ -1792,6 +1801,7 @@ describe("PUT /api/settings", () => {
1792
1801
  publicUrl: "",
1793
1802
  updateChannel: "stable",
1794
1803
  dockerAutoUpdate: false,
1804
+ proactiveKeepaliveEnabled: true,
1795
1805
  updatedAt: 999,
1796
1806
  });
1797
1807
 
@@ -1888,6 +1898,7 @@ describe("PUT /api/settings", () => {
1888
1898
  publicUrl: "https://my-server.com",
1889
1899
  updateChannel: "stable",
1890
1900
  dockerAutoUpdate: false,
1901
+ proactiveKeepaliveEnabled: true,
1891
1902
  updatedAt: 500,
1892
1903
  });
1893
1904
 
@@ -1993,6 +2004,31 @@ describe("PUT /api/settings", () => {
1993
2004
  const json = await res.json();
1994
2005
  expect(json).toEqual({ error: "dockerAutoUpdate must be a boolean" });
1995
2006
  });
2007
+
2008
+ // Validates that proactiveKeepaliveEnabled must be a boolean
2009
+ it("returns 400 for non-boolean proactiveKeepaliveEnabled", async () => {
2010
+ const res = await app.request("/api/settings", {
2011
+ method: "PUT",
2012
+ headers: { "Content-Type": "application/json" },
2013
+ body: JSON.stringify({ proactiveKeepaliveEnabled: "nope" }),
2014
+ });
2015
+ expect(res.status).toBe(400);
2016
+ const json = await res.json();
2017
+ expect(json).toEqual({ error: "proactiveKeepaliveEnabled must be a boolean" });
2018
+ });
2019
+
2020
+ // Accepts a boolean proactiveKeepaliveEnabled and forwards it to updateSettings
2021
+ it("accepts proactiveKeepaliveEnabled and forwards it to updateSettings", async () => {
2022
+ const res = await app.request("/api/settings", {
2023
+ method: "PUT",
2024
+ headers: { "Content-Type": "application/json" },
2025
+ body: JSON.stringify({ proactiveKeepaliveEnabled: false }),
2026
+ });
2027
+ expect(res.status).toBe(200);
2028
+ expect(settingsManager.updateSettings).toHaveBeenCalledWith(
2029
+ expect.objectContaining({ proactiveKeepaliveEnabled: false }),
2030
+ );
2031
+ });
1996
2032
  });
1997
2033
 
1998
2034
  describe("POST /api/settings/anthropic/verify", () => {
@@ -2114,6 +2150,7 @@ describe("GET /api/linear/issues", () => {
2114
2150
  publicUrl: "",
2115
2151
  updateChannel: "stable",
2116
2152
  dockerAutoUpdate: false,
2153
+ proactiveKeepaliveEnabled: true,
2117
2154
  updatedAt: 0,
2118
2155
  });
2119
2156
  vi.mocked(resolveApiKey).mockReturnValue(null);
@@ -2149,6 +2186,7 @@ describe("GET /api/linear/issues", () => {
2149
2186
  publicUrl: "",
2150
2187
  updateChannel: "stable",
2151
2188
  dockerAutoUpdate: false,
2189
+ proactiveKeepaliveEnabled: true,
2152
2190
  updatedAt: 0,
2153
2191
  });
2154
2192
 
@@ -2237,6 +2275,7 @@ describe("GET /api/linear/issues", () => {
2237
2275
  publicUrl: "",
2238
2276
  updateChannel: "stable",
2239
2277
  dockerAutoUpdate: false,
2278
+ proactiveKeepaliveEnabled: true,
2240
2279
  updatedAt: 0,
2241
2280
  });
2242
2281
 
@@ -2332,6 +2371,7 @@ describe("GET /api/linear/issues", () => {
2332
2371
  publicUrl: "",
2333
2372
  updateChannel: "stable",
2334
2373
  dockerAutoUpdate: false,
2374
+ proactiveKeepaliveEnabled: true,
2335
2375
  updatedAt: 0,
2336
2376
  });
2337
2377
 
@@ -2392,6 +2432,7 @@ describe("GET /api/linear/connection", () => {
2392
2432
  publicUrl: "",
2393
2433
  updateChannel: "stable",
2394
2434
  dockerAutoUpdate: false,
2435
+ proactiveKeepaliveEnabled: true,
2395
2436
  updatedAt: 0,
2396
2437
  });
2397
2438
  vi.mocked(resolveApiKey).mockReturnValue(null);
@@ -2427,6 +2468,7 @@ describe("GET /api/linear/connection", () => {
2427
2468
  publicUrl: "",
2428
2469
  updateChannel: "stable",
2429
2470
  dockerAutoUpdate: false,
2471
+ proactiveKeepaliveEnabled: true,
2430
2472
  updatedAt: 0,
2431
2473
  });
2432
2474
 
@@ -2484,6 +2526,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
2484
2526
  publicUrl: "",
2485
2527
  updateChannel: "stable",
2486
2528
  dockerAutoUpdate: false,
2529
+ proactiveKeepaliveEnabled: true,
2487
2530
  updatedAt: 0,
2488
2531
  });
2489
2532
 
@@ -2523,6 +2566,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
2523
2566
  publicUrl: "",
2524
2567
  updateChannel: "stable",
2525
2568
  dockerAutoUpdate: false,
2569
+ proactiveKeepaliveEnabled: true,
2526
2570
  updatedAt: 0,
2527
2571
  });
2528
2572
 
@@ -2561,6 +2605,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
2561
2605
  publicUrl: "",
2562
2606
  updateChannel: "stable",
2563
2607
  dockerAutoUpdate: false,
2608
+ proactiveKeepaliveEnabled: true,
2564
2609
  updatedAt: 0,
2565
2610
  });
2566
2611
  vi.mocked(resolveApiKey).mockReturnValue(null);
@@ -2601,6 +2646,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
2601
2646
  publicUrl: "",
2602
2647
  updateChannel: "stable",
2603
2648
  dockerAutoUpdate: false,
2649
+ proactiveKeepaliveEnabled: true,
2604
2650
  updatedAt: 0,
2605
2651
  });
2606
2652
 
@@ -2675,6 +2721,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
2675
2721
  publicUrl: "",
2676
2722
  updateChannel: "stable",
2677
2723
  dockerAutoUpdate: false,
2724
+ proactiveKeepaliveEnabled: true,
2678
2725
  updatedAt: 0,
2679
2726
  });
2680
2727
 
@@ -2728,6 +2775,7 @@ describe("GET /api/linear/projects", () => {
2728
2775
  publicUrl: "",
2729
2776
  updateChannel: "stable",
2730
2777
  dockerAutoUpdate: false,
2778
+ proactiveKeepaliveEnabled: true,
2731
2779
  updatedAt: 0,
2732
2780
  });
2733
2781
  vi.mocked(resolveApiKey).mockReturnValue(null);
@@ -2763,6 +2811,7 @@ describe("GET /api/linear/projects", () => {
2763
2811
  publicUrl: "",
2764
2812
  updateChannel: "stable",
2765
2813
  dockerAutoUpdate: false,
2814
+ proactiveKeepaliveEnabled: true,
2766
2815
  updatedAt: 0,
2767
2816
  });
2768
2817
 
@@ -2828,6 +2877,7 @@ describe("GET /api/linear/project-issues", () => {
2828
2877
  publicUrl: "",
2829
2878
  updateChannel: "stable",
2830
2879
  dockerAutoUpdate: false,
2880
+ proactiveKeepaliveEnabled: true,
2831
2881
  updatedAt: 0,
2832
2882
  });
2833
2883
  vi.mocked(resolveApiKey).mockReturnValue(null);
@@ -2863,6 +2913,7 @@ describe("GET /api/linear/project-issues", () => {
2863
2913
  publicUrl: "",
2864
2914
  updateChannel: "stable",
2865
2915
  dockerAutoUpdate: false,
2916
+ proactiveKeepaliveEnabled: true,
2866
2917
  updatedAt: 0,
2867
2918
  });
2868
2919
 
@@ -2943,6 +2994,7 @@ describe("GET /api/linear/project-issues", () => {
2943
2994
  publicUrl: "",
2944
2995
  updateChannel: "stable",
2945
2996
  dockerAutoUpdate: false,
2997
+ proactiveKeepaliveEnabled: true,
2946
2998
  updatedAt: 0,
2947
2999
  });
2948
3000