@hellcoder/companion 0.113.1-preview.20260728143649.6f08bbd → 0.113.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-BBi5x0sS.js → AgentsPage-DoMIvK5c.js} +1 -1
- package/dist/assets/{CronManager-B1s3pGHK.js → CronManager-DA1peJy7.js} +1 -1
- package/dist/assets/{DashboardPage-DFgvPALP.js → DashboardPage-C4B7XxBJ.js} +1 -1
- package/dist/assets/{IntegrationsPage-ClHcXTNR.js → IntegrationsPage-D4FtQBUj.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-aKHb8Kgv.js → LinearOAuthSettingsPage-BSfT0Ulr.js} +1 -1
- package/dist/assets/{LinearSettingsPage-DZbV5ES0.js → LinearSettingsPage-g23UlNTx.js} +1 -1
- package/dist/assets/{Playground-Z8gfp_Pg.js → Playground-By14cDox.js} +1 -1
- package/dist/assets/{PromptsPage-BF5QRCM7.js → PromptsPage-9YIF_Pkw.js} +1 -1
- package/dist/assets/{RunsPage-Bn7SIAEx.js → RunsPage-CeuQAzD-.js} +1 -1
- package/dist/assets/{SandboxManager-BNIPWLHz.js → SandboxManager-CoHOUMFN.js} +1 -1
- package/dist/assets/{SettingsPage-CFe6_dW9.js → SettingsPage-B6pfdIlj.js} +1 -1
- package/dist/assets/{TailscalePage-DPJGhCUO.js → TailscalePage-B6WlB5tj.js} +1 -1
- package/dist/assets/{index-B6xqr-v6.js → index-By5tzX2N.js} +8 -8
- package/dist/assets/{sw-register-A0FMc-CY.js → sw-register-E2OIBgNb.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/claude-adapter.test.ts +50 -0
- package/server/claude-adapter.ts +15 -0
- package/server/ws-bridge-types.ts +16 -0
- package/server/ws-bridge.test.ts +107 -0
- package/server/ws-bridge.ts +37 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-By5tzX2N.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-By5tzX2N.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-C86AT0KJ.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-cc-bg text-cc-fg">
|
package/dist/sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
try{self["workbox:core:7.4.0"]&&_()}catch{}const x=(a,...e)=>{let t=a;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},N=x;class l extends Error{constructor(e,t){const s=N(e,t);super(s),this.name=e,this.details=t}}const E=new Set,f={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},b=a=>[f.prefix,a,f.suffix].filter(e=>e&&e.length>0).join("-"),O=a=>{for(const e of Object.keys(f))a(e)},C={updateDetails:a=>{O(e=>{typeof a[e]=="string"&&(f[e]=a[e])})},getGoogleAnalyticsName:a=>a||b(f.googleAnalytics),getPrecacheName:a=>a||b(f.precache),getPrefix:()=>f.prefix,getRuntimeName:a=>a||b(f.runtime),getSuffix:()=>f.suffix};function P(a,e){const t=new URL(a);for(const s of e)t.searchParams.delete(s);return t.href}async function I(a,e,t,s){const n=P(e.url,t);if(e.url===n)return a.match(e,s);const r=Object.assign(Object.assign({},s),{ignoreSearch:!0}),c=await a.keys(e,r);for(const i of c){const o=P(i.url,t);if(n===o)return a.match(i,s)}}let y;function M(){if(y===void 0){const a=new Response("");if("body"in a)try{new Response(a.body),y=!0}catch{y=!1}y=!1}return y}class D{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}async function S(){for(const a of E)await a()}const W=a=>new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),"");function A(a){return new Promise(e=>setTimeout(e,a))}function K(a,e){const t=e();return a.waitUntil(t),t}async function q(a,e){let t=null;if(a.url&&(t=new URL(a.url).origin),t!==self.location.origin)throw new l("cross-origin-copy-response",{origin:t});const s=a.clone(),r={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},c=M()?s.body:await s.blob();return new Response(c,r)}function j(){self.addEventListener("activate",()=>self.clients.claim())}try{self["workbox:precaching:7.4.0"]&&_()}catch{}const H="__WB_REVISION__";function F(a){if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(typeof a=="string"){const r=new URL(a,location.href);return{cacheKey:r.href,url:r.href}}const{revision:e,url:t}=a;if(!t)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(!e){const r=new URL(t,location.href);return{cacheKey:r.href,url:r.href}}const s=new URL(t,location.href),n=new URL(t,location.href);return s.searchParams.set(H,e),{cacheKey:s.href,url:n.href}}class B{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if(e.type==="install"&&t&&t.originalRequest&&t.originalRequest instanceof Request){const n=t.originalRequest.url;s?this.notUpdatedURLs.push(n):this.updatedURLs.push(n)}return s}}}class ${constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const n=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return n?new Request(n,{headers:t.headers}):t},this._precacheController=e}}try{self["workbox:strategies:7.4.0"]&&_()}catch{}function m(a){return typeof a=="string"?new Request(a):a}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new D,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:t}=this;let s=m(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const c=await t.preloadResponse;if(c)return c}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const c of this.iterateCallbacks("requestWillFetch"))s=await c({request:s.clone(),event:t})}catch(c){if(c instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:c.message})}const r=s.clone();try{let c;c=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))c=await i({event:t,request:r,response:c});return c}catch(c){throw n&&await this.runCallbacks("fetchDidFail",{error:c,event:t,originalRequest:n.clone(),request:r.clone()}),c}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=m(e);let s;const{cacheName:n,matchOptions:r}=this._strategy,c=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(c,i);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:n,matchOptions:r,cachedResponse:s,request:c,event:this.event})||void 0;return s}async cachePut(e,t){const s=m(e);await A(0);const n=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:W(n.url)});const r=await this._ensureResponseSafeToCache(t);if(!r)return!1;const{cacheName:c,matchOptions:i}=this._strategy,o=await self.caches.open(c),h=this.hasCallback("cacheDidUpdate"),p=h?await I(o,n.clone(),["__WB_REVISION__"],i):null;try{await o.put(n,h?r.clone():r)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await S(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:c,oldResponse:p,newResponse:r.clone(),request:n,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const r of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await r({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield r=>{const c=Object.assign(Object.assign({},r),{state:s});return t[e](c)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){const e=this._extendLifetimePromises.splice(0),s=(await Promise.allSettled(e)).find(n=>n.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=await n({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t}}class V{constructor(e={}){this.cacheName=C.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,n="params"in e?e.params:void 0,r=new G(this,{event:t,request:s,params:n}),c=this._getResponse(r,s,t),i=this._awaitComplete(c,r,s,t);return[c,i]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=await this._handle(t,e),!n||n.type==="error")throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(const c of e.iterateCallbacks("handlerDidError"))if(n=await c({error:r,event:s,request:t}),n)break}if(!n)throw r}for(const r of e.iterateCallbacks("handlerWillRespond"))n=await r({event:s,request:t,response:n});return n}async _awaitComplete(e,t,s,n){let r,c;try{r=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await t.doneWaiting()}catch(i){i instanceof Error&&(c=i)}if(await t.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:c}),t.destroy(),c)throw c}}class d extends V{constructor(e={}){e.cacheName=C.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(d.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&t.event.type==="install"?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s;const n=t.params||{};if(this._fallbackToNetwork){const r=n.integrity,c=e.integrity,i=!c||c===r;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?c||r:void 0})),r&&i&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new l("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();const s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,n]of this.plugins.entries())n!==d.copyRedirectedCacheableResponsesPlugin&&(n===d.defaultPrecacheCacheabilityPlugin&&(e=s),n.cacheWillUpdate&&t++);t===0?this.plugins.push(d.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}d.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:a}){return!a||a.status>=400?null:a}};d.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:a}){return a.redirected?await q(a):a}};class Q{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new d({cacheName:C.getPrecacheName(e),plugins:[...t,new $({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const t=[];for(const s of e){typeof s=="string"?t.push(s):s&&s.revision===void 0&&t.push(s.url);const{cacheKey:n,url:r}=F(s),c=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(r)&&this._urlsToCacheKeys.get(r)!==n)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(r),secondEntry:n});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(n)&&this._cacheKeysToIntegrities.get(n)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:r});this._cacheKeysToIntegrities.set(n,s.integrity)}if(this._urlsToCacheKeys.set(r,n),this._urlsToCacheModes.set(r,c),t.length>0){const i=`Workbox is precaching URLs without revision info: ${t.join(", ")}
|
|
2
|
-
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"
|
|
2
|
+
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"f61d2f9dcbdeb06894be02d8dba85fd3","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-E2OIBgNb.js"},{"revision":null,"url":"assets/index-C86AT0KJ.css"},{"revision":null,"url":"assets/index-By5tzX2N.js"},{"revision":null,"url":"assets/TailscalePage-B6WlB5tj.js"},{"revision":null,"url":"assets/SettingsPage-B6pfdIlj.js"},{"revision":null,"url":"assets/SandboxManager-CoHOUMFN.js"},{"revision":null,"url":"assets/RunsPage-CeuQAzD-.js"},{"revision":null,"url":"assets/PromptsPage-9YIF_Pkw.js"},{"revision":null,"url":"assets/Playground-By14cDox.js"},{"revision":null,"url":"assets/LinearSettingsPage-g23UlNTx.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-BSfT0Ulr.js"},{"revision":null,"url":"assets/IntegrationsPage-D4FtQBUj.js"},{"revision":null,"url":"assets/DashboardPage-C4B7XxBJ.js"},{"revision":null,"url":"assets/CronManager-DA1peJy7.js"},{"revision":null,"url":"assets/AgentsPage-DoMIvK5c.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.113.1
|
|
3
|
+
"version": "0.113.1",
|
|
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",
|
|
@@ -1812,6 +1812,10 @@ describe("stdio silence probe", () => {
|
|
|
1812
1812
|
adapter.attachStdio(proc);
|
|
1813
1813
|
expect(adapter.isConnected()).toBe(true);
|
|
1814
1814
|
|
|
1815
|
+
// A turn must be outstanding: the probe no longer escalates on an idle
|
|
1816
|
+
// session, because silence there just means the user has not typed.
|
|
1817
|
+
adapter.send({ type: "user_message", content: "do something" });
|
|
1818
|
+
|
|
1815
1819
|
// Silence short of the probe threshold: nothing happens yet.
|
|
1816
1820
|
await vi.advanceTimersByTimeAsync(110_000);
|
|
1817
1821
|
expect(disconnectCb).not.toHaveBeenCalled();
|
|
@@ -1893,12 +1897,58 @@ describe("stdio silence probe", () => {
|
|
|
1893
1897
|
expect(adapter.isConnected()).toBe(true);
|
|
1894
1898
|
});
|
|
1895
1899
|
|
|
1900
|
+
/**
|
|
1901
|
+
* Idle sessions must never be torn down by the probe.
|
|
1902
|
+
*
|
|
1903
|
+
* Silence on an idle session means the user has not typed — indistinguishable
|
|
1904
|
+
* from a dead turn by elapsed time alone. Escalating there produced "Response
|
|
1905
|
+
* interrupted — the backend disconnected" banners for turns that never
|
|
1906
|
+
* existed, which is most of the disconnect noise users report. Observed on
|
|
1907
|
+
* this very session: `CLI silent; sending liveness probe silentForMs=133784`
|
|
1908
|
+
* while simply waiting for input.
|
|
1909
|
+
*/
|
|
1910
|
+
it("never escalates on an idle session with no turn outstanding", async () => {
|
|
1911
|
+
vi.useFakeTimers();
|
|
1912
|
+
const { proc } = createMockProc();
|
|
1913
|
+
adapter.attachStdio(proc);
|
|
1914
|
+
|
|
1915
|
+
// Ten minutes of silence with no user message ever sent.
|
|
1916
|
+
await vi.advanceTimersByTimeAsync(600_000);
|
|
1917
|
+
|
|
1918
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1919
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
it("stops escalating once the turn completes", async () => {
|
|
1923
|
+
// The turn ends mid-silence: what was a recoverable stall becomes an idle
|
|
1924
|
+
// session, and must stop being a kill candidate.
|
|
1925
|
+
vi.useFakeTimers();
|
|
1926
|
+
const { proc, pushStdout } = createMockProc();
|
|
1927
|
+
adapter.attachStdio(proc);
|
|
1928
|
+
adapter.send({ type: "user_message", content: "quick question" });
|
|
1929
|
+
|
|
1930
|
+
pushStdout(JSON.stringify({
|
|
1931
|
+
type: "result", subtype: "success", is_error: false,
|
|
1932
|
+
duration_ms: 5, num_turns: 1, session_id: "s", uuid: "r-idle",
|
|
1933
|
+
}) + "\n");
|
|
1934
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
1935
|
+
|
|
1936
|
+
await vi.advanceTimersByTimeAsync(600_000);
|
|
1937
|
+
|
|
1938
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1939
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1896
1942
|
it("still escalates when a tool call blows the grace ceiling", async () => {
|
|
1897
1943
|
// A tool that never returns must not suppress recovery forever.
|
|
1898
1944
|
vi.useFakeTimers();
|
|
1899
1945
|
const { proc, pushStdout } = createMockProc();
|
|
1900
1946
|
adapter.attachStdio(proc);
|
|
1901
1947
|
|
|
1948
|
+
// Establish an outstanding turn — escalation is gated on one, so that an
|
|
1949
|
+
// idle session is never torn down for simply having nothing to say.
|
|
1950
|
+
adapter.send({ type: "user_message", content: "run a tool" });
|
|
1951
|
+
|
|
1902
1952
|
pushStdout(
|
|
1903
1953
|
JSON.stringify({
|
|
1904
1954
|
type: "assistant",
|
package/server/claude-adapter.ts
CHANGED
|
@@ -665,6 +665,21 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
665
665
|
return;
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
// Nothing to rescue when no turn is outstanding. An idle session is silent
|
|
669
|
+
// because the user has not typed, which is indistinguishable from a dead
|
|
670
|
+
// turn by elapsed time alone — so probing it produced "Response interrupted
|
|
671
|
+
// — the backend disconnected" banners for turns that never existed. That is
|
|
672
|
+
// most of the disconnect noise users report.
|
|
673
|
+
//
|
|
674
|
+
// Recovery is not lost: a message sent to a terminated session already
|
|
675
|
+
// requests a relaunch and flushes on attach, so a dead idle CLI is revived
|
|
676
|
+
// on demand rather than pre-emptively. Same trade as the browser-aware
|
|
677
|
+
// keepalive — pay the cost when someone is actually waiting.
|
|
678
|
+
if (!this.turnInFlight) {
|
|
679
|
+
this.probeSentAt = null;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
668
683
|
// An unanswered probe is the actual failure signal: a healthy CLI replies
|
|
669
684
|
// to control requests promptly even while working.
|
|
670
685
|
if (this.probeSentAt !== null) {
|
|
@@ -75,6 +75,22 @@ export interface Session {
|
|
|
75
75
|
* new user_message is dispatched.
|
|
76
76
|
*/
|
|
77
77
|
inFlightTurnReplayed?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* True from the moment a user message is dispatched until its `result`
|
|
80
|
+
* arrives, regardless of how much output was produced in between.
|
|
81
|
+
*
|
|
82
|
+
* `inFlightUserTurn` is cleared as soon as the first `assistant`/
|
|
83
|
+
* `stream_event` lands, so it only ever covers turns that die BEFORE
|
|
84
|
+
* producing output. A turn interrupted mid-answer had already cleared it and
|
|
85
|
+
* was therefore never recovered — the session simply idled until a human
|
|
86
|
+
* typed "continue". This flag is what makes that case detectable.
|
|
87
|
+
*/
|
|
88
|
+
turnAwaitingResult?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* One-shot guard for the mid-answer continuation nudge, so a turn that keeps
|
|
91
|
+
* dying cannot be nudged in a loop. Reset when a new user_message is sent.
|
|
92
|
+
*/
|
|
93
|
+
continuationSent?: boolean;
|
|
78
94
|
/**
|
|
79
95
|
* Set when the last turn failed authentication (expired/invalid credentials).
|
|
80
96
|
* Auto-relaunch is skipped while this is set — relaunching cannot fix auth —
|
package/server/ws-bridge.test.ts
CHANGED
|
@@ -2040,6 +2040,113 @@ describe("CLI message routing", () => {
|
|
|
2040
2040
|
}
|
|
2041
2041
|
});
|
|
2042
2042
|
|
|
2043
|
+
/**
|
|
2044
|
+
* Recovery for turns interrupted MID-ANSWER.
|
|
2045
|
+
*
|
|
2046
|
+
* `inFlightUserTurn` is cleared on the first assistant/stream_event, so the
|
|
2047
|
+
* existing replay only ever covered turns that died before producing output.
|
|
2048
|
+
* A turn cut off mid-answer was not recovered at all: the session idled until
|
|
2049
|
+
* a human typed "continue" by hand. Reported in production as 15 wedge-kills
|
|
2050
|
+
* across 11 sessions in ~34h.
|
|
2051
|
+
*/
|
|
2052
|
+
describe("mid-answer turn continuation", () => {
|
|
2053
|
+
async function startTurnWithOutput(sessionId: string) {
|
|
2054
|
+
const cli = makeCliSocket(sessionId);
|
|
2055
|
+
bridge.handleCLIOpen(cli, sessionId);
|
|
2056
|
+
await bridge.handleCLIMessage(cli, makeInitMsg());
|
|
2057
|
+
const browser = makeBrowserSocket(sessionId);
|
|
2058
|
+
bridge.handleBrowserOpen(browser, sessionId);
|
|
2059
|
+
|
|
2060
|
+
await bridge.handleBrowserMessage(browser, JSON.stringify({
|
|
2061
|
+
type: "user_message",
|
|
2062
|
+
content: "do a big task",
|
|
2063
|
+
}));
|
|
2064
|
+
// First output clears inFlightUserTurn — this is what made the case
|
|
2065
|
+
// invisible to the original replay logic.
|
|
2066
|
+
await bridge.handleCLIMessage(cli, JSON.stringify({
|
|
2067
|
+
type: "stream_event",
|
|
2068
|
+
event: { type: "content_block_delta", delta: { type: "text_delta", text: "work" } },
|
|
2069
|
+
session_id: "cli-123",
|
|
2070
|
+
uuid: "se-1",
|
|
2071
|
+
}));
|
|
2072
|
+
return { cli, browser, session: bridge.getSession(sessionId)! };
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
it("nudges a turn that was interrupted after it began answering", async () => {
|
|
2076
|
+
const { session } = await startTurnWithOutput("s1");
|
|
2077
|
+
expect(session.inFlightUserTurn).toBeUndefined();
|
|
2078
|
+
expect(session.turnAwaitingResult).toBe(true);
|
|
2079
|
+
|
|
2080
|
+
// Relaunch: a fresh adapter attaches.
|
|
2081
|
+
const sent: string[] = [];
|
|
2082
|
+
bridge.attachBackendAdapter("s1", {
|
|
2083
|
+
isConnected: () => true,
|
|
2084
|
+
send: (m: unknown) => { sent.push(typeof m === "string" ? m : JSON.stringify(m)); return true; },
|
|
2085
|
+
disconnect: async () => {},
|
|
2086
|
+
onBrowserMessage: () => {},
|
|
2087
|
+
onSessionMeta: () => {},
|
|
2088
|
+
onDisconnect: () => {},
|
|
2089
|
+
onInitError: () => {},
|
|
2090
|
+
} as never, "claude");
|
|
2091
|
+
|
|
2092
|
+
const nudge = sent.map((m) => JSON.parse(m)).find((m) => m.type === "user_message");
|
|
2093
|
+
expect(nudge, `sent: ${JSON.stringify(sent)}`).toBeDefined();
|
|
2094
|
+
expect(nudge.content).toMatch(/continue from where you left off/i);
|
|
2095
|
+
// It must not re-send the original prompt, which would redo tool work.
|
|
2096
|
+
expect(nudge.content).not.toBe("do a big task");
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
it("does not nudge twice for the same turn", async () => {
|
|
2100
|
+
// A turn that keeps dying must not accumulate nudges.
|
|
2101
|
+
const { session } = await startTurnWithOutput("s1");
|
|
2102
|
+
const sent: string[] = [];
|
|
2103
|
+
const attach = () => bridge.attachBackendAdapter("s1", {
|
|
2104
|
+
isConnected: () => true,
|
|
2105
|
+
send: (m: unknown) => { sent.push(typeof m === "string" ? m : JSON.stringify(m)); return true; },
|
|
2106
|
+
disconnect: async () => {},
|
|
2107
|
+
onBrowserMessage: () => {},
|
|
2108
|
+
onSessionMeta: () => {},
|
|
2109
|
+
onDisconnect: () => {},
|
|
2110
|
+
onInitError: () => {},
|
|
2111
|
+
} as never, "claude");
|
|
2112
|
+
|
|
2113
|
+
attach();
|
|
2114
|
+
attach();
|
|
2115
|
+
|
|
2116
|
+
const nudges = sent
|
|
2117
|
+
.map((m) => JSON.parse(m))
|
|
2118
|
+
.filter((m) => m.type === "user_message" && /continue from where/i.test(m.content || ""));
|
|
2119
|
+
expect(nudges).toHaveLength(1);
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
it("does not nudge once the turn has completed", async () => {
|
|
2123
|
+
// A finished turn is not an interrupted one — nudging here would send an
|
|
2124
|
+
// unprompted message to an idle session.
|
|
2125
|
+
const { cli, session } = await startTurnWithOutput("s1");
|
|
2126
|
+
await bridge.handleCLIMessage(cli, JSON.stringify({
|
|
2127
|
+
type: "result", subtype: "success", is_error: false,
|
|
2128
|
+
duration_ms: 10, num_turns: 1, session_id: "cli-123", uuid: "r-1",
|
|
2129
|
+
}));
|
|
2130
|
+
expect(session.turnAwaitingResult).toBe(false);
|
|
2131
|
+
|
|
2132
|
+
const sent: string[] = [];
|
|
2133
|
+
bridge.attachBackendAdapter("s1", {
|
|
2134
|
+
isConnected: () => true,
|
|
2135
|
+
send: (m: unknown) => { sent.push(typeof m === "string" ? m : JSON.stringify(m)); return true; },
|
|
2136
|
+
disconnect: async () => {},
|
|
2137
|
+
onBrowserMessage: () => {},
|
|
2138
|
+
onSessionMeta: () => {},
|
|
2139
|
+
onDisconnect: () => {},
|
|
2140
|
+
onInitError: () => {},
|
|
2141
|
+
} as never, "claude");
|
|
2142
|
+
|
|
2143
|
+
const nudges = sent
|
|
2144
|
+
.map((m) => JSON.parse(m))
|
|
2145
|
+
.filter((m) => m.type === "user_message" && /continue from where/i.test(m.content || ""));
|
|
2146
|
+
expect(nudges).toHaveLength(0);
|
|
2147
|
+
});
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2043
2150
|
it("tool_progress: broadcasts", async () => {
|
|
2044
2151
|
const msg = JSON.stringify({
|
|
2045
2152
|
type: "tool_progress",
|
package/server/ws-bridge.ts
CHANGED
|
@@ -527,6 +527,8 @@ export class WsBridge {
|
|
|
527
527
|
const resultData = msg.data;
|
|
528
528
|
// The turn completed (even if it errored) — no replay needed.
|
|
529
529
|
session.inFlightUserTurn = undefined;
|
|
530
|
+
// The turn is genuinely finished, so a later relaunch must not nudge it.
|
|
531
|
+
session.turnAwaitingResult = false;
|
|
530
532
|
// Detect auth failures (expired/invalid credentials). The CLI keeps
|
|
531
533
|
// running in stream-json mode, so this is the primary auth signal — not
|
|
532
534
|
// a process exit. Flag the session so auto-relaunch is skipped (the
|
|
@@ -729,6 +731,36 @@ export class WsBridge {
|
|
|
729
731
|
type: "error",
|
|
730
732
|
message: "Reconnected — re-sending your last message, which the previous session didn't answer.",
|
|
731
733
|
});
|
|
734
|
+
} else if (
|
|
735
|
+
!isLegacyWsClaude &&
|
|
736
|
+
// Only when output had already begun: inFlightUserTurn is cleared by the
|
|
737
|
+
// first token, so its absence is what distinguishes "died mid-answer"
|
|
738
|
+
// from "died before answering". Without this check a poison prompt that
|
|
739
|
+
// has exhausted its one replay would get nudged on every subsequent
|
|
740
|
+
// relaunch, defeating inFlightTurnReplayed.
|
|
741
|
+
!session.inFlightUserTurn &&
|
|
742
|
+
session.turnAwaitingResult &&
|
|
743
|
+
!session.continuationSent
|
|
744
|
+
) {
|
|
745
|
+
// The turn died AFTER it had begun answering. `--resume` restores the
|
|
746
|
+
// transcript but does not restart the turn, so without this the session
|
|
747
|
+
// idles forever and a human eventually types "continue" by hand.
|
|
748
|
+
//
|
|
749
|
+
// Re-sending the original prompt is wrong here: work already happened and
|
|
750
|
+
// replaying it would redo tool calls. A continuation nudge asks the model
|
|
751
|
+
// to finish from where the transcript stops instead.
|
|
752
|
+
session.continuationSent = true;
|
|
753
|
+
session.pendingMessages.unshift(JSON.stringify({
|
|
754
|
+
type: "user_message",
|
|
755
|
+
content: "Your previous response was interrupted before it finished. Continue from where you left off — do not repeat work that is already in the transcript.",
|
|
756
|
+
}));
|
|
757
|
+
log.info("ws-bridge", "Nudging interrupted turn to continue after relaunch", {
|
|
758
|
+
sessionId,
|
|
759
|
+
});
|
|
760
|
+
this.broadcastToBrowsers(session, {
|
|
761
|
+
type: "error",
|
|
762
|
+
message: "Reconnected — the previous answer was cut off, asking it to continue.",
|
|
763
|
+
});
|
|
732
764
|
}
|
|
733
765
|
|
|
734
766
|
// Flush pending messages for server-owned backends (Codex + stdio Claude),
|
|
@@ -1257,6 +1289,11 @@ export class WsBridge {
|
|
|
1257
1289
|
// one-shot replay guard.
|
|
1258
1290
|
session.inFlightUserTurn = JSON.stringify(msg);
|
|
1259
1291
|
session.inFlightTurnReplayed = false;
|
|
1292
|
+
// Tracked separately from inFlightUserTurn, which is cleared on the first
|
|
1293
|
+
// token: this stays set until the turn actually finishes, so an interrupt
|
|
1294
|
+
// *mid-answer* is still recognisable as an unfinished turn.
|
|
1295
|
+
session.turnAwaitingResult = true;
|
|
1296
|
+
session.continuationSent = false;
|
|
1260
1297
|
// A fresh turn is an attempt to make progress (e.g. after the user
|
|
1261
1298
|
// re-authenticated), so clear any stale auth-block — a genuine crash on
|
|
1262
1299
|
// this new turn should be allowed to auto-relaunch again.
|