@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.
@@ -1 +1 @@
1
- import{_ as u}from"./index-B6xqr-v6.js";function w(r={}){const{immediate:i=!1,onNeedReload:d,onNeedRefresh:_,onOfflineReady:s,onRegistered:n,onRegisteredSW:o,onRegisterError:t}=r;let a,c;const l=async(e=!0)=>{await c};async function f(){if("serviceWorker"in navigator){if(a=await u(async()=>{const{Workbox:e}=await import("./workbox-window.prod.es5-BBnX5xw4.js");return{Workbox:e}},[]).then(({Workbox:e})=>new e("/sw.js",{scope:"/",type:"classic"})).catch(e=>{t==null||t(e)}),!a)return;a.addEventListener("activated",e=>{(e.isUpdate||e.isExternal)&&(d?d():window.location.reload())}),a.addEventListener("installed",e=>{e.isUpdate||s==null||s()}),a.register({immediate:i}).then(e=>{o?o("/sw.js",e):n==null||n(e)}).catch(e=>{t==null||t(e)})}}return c=f(),l}w({onRegisteredSW(r,i){i&&setInterval(()=>{i.update()},3600*1e3)},onOfflineReady(){console.log("[SW] Offline-ready: all assets precached")}});
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-B6xqr-v6.js"></script>
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":"4f8bb842832a3076ea5fd36837194001","url":"index.html"},{"revision":"1421e9c6cad8218ef5689b85034d3a95","url":"icon-512.png"},{"revision":"b96da5a0b6636012517f54b0e69c304d","url":"icon-192.png"},{"revision":"c64ca8ae8596d4ffe13d0f591beb2274","url":"favicon.svg"},{"revision":"6481face04bd08e6f0bfc2a633840b57","url":"apple-touch-icon.png"},{"revision":"83304194a3c8be2b5b61242eeb1c1046","url":"fonts/MesloLGSNerdFontMono-Regular.woff2"},{"revision":"60fafe18cbcb717c51cdabf87f9490f0","url":"fonts/MesloLGSNerdFontMono-Bold.woff2"},{"revision":null,"url":"assets/workbox-window.prod.es5-BBnX5xw4.js"},{"revision":null,"url":"assets/sw-register-A0FMc-CY.js"},{"revision":null,"url":"assets/index-C86AT0KJ.css"},{"revision":null,"url":"assets/index-B6xqr-v6.js"},{"revision":null,"url":"assets/TailscalePage-DPJGhCUO.js"},{"revision":null,"url":"assets/SettingsPage-CFe6_dW9.js"},{"revision":null,"url":"assets/SandboxManager-BNIPWLHz.js"},{"revision":null,"url":"assets/RunsPage-Bn7SIAEx.js"},{"revision":null,"url":"assets/PromptsPage-BF5QRCM7.js"},{"revision":null,"url":"assets/Playground-Z8gfp_Pg.js"},{"revision":null,"url":"assets/LinearSettingsPage-DZbV5ES0.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-aKHb8Kgv.js"},{"revision":null,"url":"assets/IntegrationsPage-ClHcXTNR.js"},{"revision":null,"url":"assets/DashboardPage-DFgvPALP.js"},{"revision":null,"url":"assets/CronManager-B1s3pGHK.js"},{"revision":null,"url":"assets/AgentsPage-BBi5x0sS.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
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-preview.20260728143649.6f08bbd",
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",
@@ -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 —
@@ -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",
@@ -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.