@coool/opencode-adaptive 1.2.2 → 1.3.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 (47) hide show
  1. package/dist/gui-experimental.cjs +3 -1
  2. package/dist/gui.d.ts +2 -1
  3. package/dist/gui.js +7 -5
  4. package/dist/gui.js.map +1 -1
  5. package/dist/health/classifier/classify.js +17 -1
  6. package/dist/health/classifier/classify.js.map +1 -1
  7. package/dist/supervisor/supervisor.js +4 -1
  8. package/dist/supervisor/supervisor.js.map +1 -1
  9. package/dist/tui.js +5 -3
  10. package/dist/tui.js.map +1 -1
  11. package/dist/ui/{tui → flow}/controller.d.ts +5 -8
  12. package/dist/ui/{tui → flow}/controller.js +4 -9
  13. package/dist/ui/flow/controller.js.map +1 -0
  14. package/dist/ui/flow/menu.js.map +1 -0
  15. package/dist/ui/flow/options.d.ts +12 -0
  16. package/dist/ui/flow/options.js.map +1 -0
  17. package/dist/ui/{tui → flow}/receipts.d.ts +1 -1
  18. package/dist/ui/{tui → flow}/receipts.js +4 -7
  19. package/dist/ui/flow/receipts.js.map +1 -0
  20. package/dist/ui/{tui → flow}/status.d.ts +0 -2
  21. package/dist/ui/{tui → flow}/status.js +0 -6
  22. package/dist/ui/flow/status.js.map +1 -0
  23. package/dist/ui/{tui → flow}/tiers.d.ts +2 -9
  24. package/dist/ui/{tui → flow}/tiers.js +5 -25
  25. package/dist/ui/flow/tiers.js.map +1 -0
  26. package/dist/ui/gui/api.d.ts +6 -2
  27. package/dist/ui/gui/api.js +4 -4
  28. package/dist/ui/gui/api.js.map +1 -1
  29. package/dist/ui/gui/flow.d.ts +23 -0
  30. package/dist/ui/gui/flow.js +123 -0
  31. package/dist/ui/gui/flow.js.map +1 -0
  32. package/dist/ui/gui/view.js +2 -2
  33. package/dist/ui/gui/view.js.map +1 -1
  34. package/dist/ui/tui/search.d.ts +9 -0
  35. package/dist/ui/tui/search.js +10 -0
  36. package/dist/ui/tui/search.js.map +1 -0
  37. package/package.json +2 -1
  38. package/dist/ui/tui/controller.js.map +0 -1
  39. package/dist/ui/tui/menu.js.map +0 -1
  40. package/dist/ui/tui/options.d.ts +0 -19
  41. package/dist/ui/tui/options.js.map +0 -1
  42. package/dist/ui/tui/receipts.js.map +0 -1
  43. package/dist/ui/tui/status.js.map +0 -1
  44. package/dist/ui/tui/tiers.js.map +0 -1
  45. /package/dist/ui/{tui → flow}/menu.d.ts +0 -0
  46. /package/dist/ui/{tui → flow}/menu.js +0 -0
  47. /package/dist/ui/{tui → flow}/options.js +0 -0
@@ -1 +1,3 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const n=require("solid-js/web"),w=require("@opencode/plugin/gui"),u=require("solid-js"),M=require("@opencode/schema/rpc"),q=128,L="coool.adaptive",C="coool.adaptive",l={internal:f()};function r(e,t){return{"~standard":{version:1,vendor:C,validate:a=>e(a)?{value:a}:{issues:[{message:t}]}}}}function f(){return{"~standard":{version:1,vendor:C,validate:e=>e===void 0?{value:void 0}:{issues:[{message:"unexpected input: no input expected"}]}}}}const i=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),g=e=>typeof e=="string",m=e=>{if(!(i(e)&&g(e.sessionID)))return!1;const t=e.sessionID;return t.length>0&&t.length<=q},N=e=>i(e)&&g(e.sessionID)&&(e.mode===null||g(e.mode)),V=e=>i(e)&&g(e.sessionID)&&(e.tier===null||g(e.tier)),F=M.Rpc.define({id:L,methods:{"settings.get":{errors:l,input:f(),output:r(i,"output must be an object")},"settings.set":{errors:l,input:r(i,"input must be an object"),output:r(i,"output must be an object")},"status.get":{errors:l,input:f(),output:r(i,"output must be an object")},"receipts.list":{errors:l,input:f(),output:r(Array.isArray,"output must be an array")},"session-mode.get":{errors:l,input:r(m,"input must be an object with a sessionID string"),output:r(i,"output must be an object")},"session-mode.set":{errors:l,input:r(N,"input must be an object with a sessionID string and a mode string or null"),output:r(i,"output must be an object")},"tier-request.get":{errors:l,input:r(m,"input must be an object with a sessionID string"),output:r(i,"output must be an object")},"tier-request.set":{errors:l,input:r(V,"input must be an object with a sessionID string and a tier string or null"),output:r(i,"output must be an object")}},events:{"settings-changed":{schema:r(i,"event data must be an object")},"session-mode-changed":{schema:r(m,"event data must be an object with a sessionID string")},"tier-request-changed":{schema:r(m,"event data must be an object with a sessionID string")}}}),D=["off","observe","active"],G=["quick","standard","advanced"];function W(e){const t=e.rpc(F);return{status:()=>t["status.get"]().catch(()=>{}),receipts:()=>t["receipts.list"]().catch(()=>{}),update:async a=>{try{return(await t["settings.set"](a)).ok}catch{return!1}},watchSettings:a=>t.events.on("settings-changed",()=>{a()})}}function $(e){const t=`${e.providerID}/${e.id}`;return e.variant===void 0?t:`${t}@${e.variant}`}function x(e){if(e===void 0)return"";const t=[];return e.health!=="healthy"&&t.push(e.health),e.breaker==="open"?t.push("breaker-open"):e.breaker==="half_open"&&t.push("half-open"),e.windowed&&t.push("windowed"),t.length===0?"":` [${t.join(", ")}]`}function j(e){switch(e.kind){case"apply":{const t=e.tier===void 0?"":` [${e.tier}]`;return`apply ${$(e.model)}${t} (${e.reason})`}case"defer":{const t=e.detail===void 0?"":` — ${e.detail}`;return`defer: ${e.reason}${t}`}case"park":{const t=e.detail===void 0?"":` — ${e.detail}`;return`park: ${e.reason}${t}`}case"escalate":return`escalate ${e.step} → ${$(e.target)} [${e.tier}]`;case"observe-only":return`observe: ${j(e.would)}`}}function z(e){return e===void 0?"pending":e.status==="succeeded"?"succeeded":e.status==="failed"?`failed (${e.category})`:`interrupted (${e.reason})`}function H(e){return`#${String(e.id)} ${j(e.decision)} — ${z(e.outcome)}`}const J="connection";function X(e){if(e===void 0)return[{id:J,label:"Server",value:"disconnected",tone:"warning"}];const t=[{id:"mode",label:"Mode",value:e.mode},{id:"pin",label:"Pin",value:e.pin===null?"none":$(e.pin)}];for(const a of G){const c=e.tiers[a],S=c.length===0?"empty":c.map(b=>`${$(b.ref)}${x(b)}`).join(", ");t.push({id:`tier-${a}`,label:`Tier ${a}`,value:S})}return t.push({id:"parked",label:"Parked",value:String(e.parked)}),t.push({id:"receipts",label:"Receipts",value:String(e.receipts)}),t}function B(e){return D.map(t=>({value:t,title:t===e?`${t} (current)`:t}))}function K(e){return D.find(t=>t===e)}function Q(e){return e.map(t=>H(t))}var U=n.template("<span class=adaptive-saved role=status>Saved"),Y=n.template("<div class=adaptive-receipts>"),Z=n.template('<div class=adaptive-page><table class=adaptive-status aria-label="OpenCode Adaptive status"><tbody></tbody></table><label class=adaptive-mode><span class=adaptive-row-label>Mode</span><select class=adaptive-mode-select>'),ee=n.template("<tr class=adaptive-row><td class=adaptive-row-label></td><td class=adaptive-row-value>"),te=n.template("<option>"),ne=n.template("<div class=adaptive-receipt-line>");const re=5;function y(e){const[t,a]=u.createSignal(void 0),[c,S]=u.createSignal("observe"),[b,R]=u.createSignal([]),[A,E]=u.createSignal(!1),_=()=>{e.api.status().then(s=>{a(s),s!==void 0&&S(s.mode)}),e.api.receipts().then(s=>{R(s??[])})};u.onCleanup(e.api.watchSettings(_)),_();const P=s=>{const p=K(s.value);s.value=c(),p!==void 0&&e.api.update({mode:p}).then(I=>{E(I),_()})};return(()=>{var s=Z(),p=s.firstChild,I=p.firstChild,T=p.nextSibling,k=T.firstChild,h=k.nextSibling;return n.insert(I,n.createComponent(u.For,{get each(){return X(t())},children:o=>(()=>{var d=ee(),v=d.firstChild,O=v.nextSibling;return n.insert(v,()=>o.label),n.insert(O,()=>o.value),n.effect(()=>n.setAttribute(d,"data-tone",o.tone)),d})()})),h.addEventListener("change",o=>{P(o.currentTarget)}),n.insert(h,n.createComponent(u.For,{get each(){return B(c())},children:o=>(()=>{var d=te();return n.insert(d,()=>o.title),n.effect(()=>d.value=o.value),d})()})),n.insert(s,n.createComponent(u.Show,{get when(){return A()},get children(){return U()}}),null),n.insert(s,n.createComponent(u.Show,{get when(){return t()!==void 0},get children(){var o=Y();return n.insert(o,n.createComponent(u.For,{get each(){return Q(b()).slice(0,re)},children:d=>(()=>{var v=ne();return n.insert(v,d),v})()})),o}}),null),n.effect(()=>h.disabled=t()===void 0),n.effect(()=>h.value=c()),s})()}const se=w.Plugin.define({setup(e){const t=W(e.client);e.add(w.SettingsPage,{id:"adaptive",title:"OpenCode Adaptive",entries:[{id:"adaptive",title:"OpenCode Adaptive",description:"Execution tier routing, operating mode and resource health"}],render:()=>n.createComponent(y,{api:t})}),e.add(w.Command,{id:"open",title:"OpenCode Adaptive",group:"Adaptive",run:()=>{e.layout.settings("adaptive")}})}});exports.AdaptiveStatusPage=y;exports.default=se;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),O=require("@opencode/plugin/gui"),m=require("solid-js"),X=require("@opencode/schema/rpc"),Z=128,K="coool.adaptive",N="coool.adaptive",S={internal:M()};function l(e,t){return{"~standard":{version:1,vendor:N,validate:i=>e(i)?{value:i}:{issues:[{message:t}]}}}}function M(){return{"~standard":{version:1,vendor:N,validate:e=>e===void 0?{value:void 0}:{issues:[{message:"unexpected input: no input expected"}]}}}}const v=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),T=e=>typeof e=="string",D=e=>{if(!(v(e)&&T(e.sessionID)))return!1;const t=e.sessionID;return t.length>0&&t.length<=Z},Q=e=>v(e)&&T(e.sessionID)&&(e.mode===null||T(e.mode)),ee=e=>v(e)&&T(e.sessionID)&&(e.tier===null||T(e.tier)),x=X.Rpc.define({id:K,methods:{"settings.get":{errors:S,input:M(),output:l(v,"output must be an object")},"settings.set":{errors:S,input:l(v,"input must be an object"),output:l(v,"output must be an object")},"status.get":{errors:S,input:M(),output:l(v,"output must be an object")},"receipts.list":{errors:S,input:M(),output:l(Array.isArray,"output must be an array")},"session-mode.get":{errors:S,input:l(D,"input must be an object with a sessionID string"),output:l(v,"output must be an object")},"session-mode.set":{errors:S,input:l(Q,"input must be an object with a sessionID string and a mode string or null"),output:l(v,"output must be an object")},"tier-request.get":{errors:S,input:l(D,"input must be an object with a sessionID string"),output:l(v,"output must be an object")},"tier-request.set":{errors:S,input:l(ee,"input must be an object with a sessionID string and a tier string or null"),output:l(v,"output must be an object")}},events:{"settings-changed":{schema:l(v,"event data must be an object")},"session-mode-changed":{schema:l(D,"event data must be an object with a sessionID string")},"tier-request-changed":{schema:l(D,"event data must be an object with a sessionID string")}}}),E=["off","observe","active"],w=["quick","standard","advanced"];function te(e,t){const i=e.rpc(x);return{status:()=>i["status.get"](void 0,t()).catch(()=>{}),receipts:()=>i["receipts.list"](void 0,t()).catch(()=>{}),update:async n=>{try{return(await i["settings.set"](n,t())).ok}catch{return!1}},watchSettings:n=>i.events.on("settings-changed",()=>{n()})}}function f(e){const t=`${e.providerID}/${e.id}`;return e.variant===void 0?t:`${t}@${e.variant}`}function L(e){if(e===void 0)return"";const t=[];return e.health!=="healthy"&&t.push(e.health),e.breaker==="open"?t.push("breaker-open"):e.breaker==="half_open"&&t.push("half-open"),e.windowed&&t.push("windowed"),t.length===0?"":` [${t.join(", ")}]`}function ie(e,t,i){const n=[{title:"Add model…",value:{kind:"add"},description:"Search OpenCode models"}],o=i==null?void 0:i.tiers[t];return e.tiers[t].forEach((r,s)=>{n.push({title:`${f(r)}${L(o==null?void 0:o[s])}`,value:{kind:"resource",index:s,ref:r},description:"Move or remove"})}),n}function ne(e){return w.map(t=>{const i=e.tiers[t],n=i.length===0?"(empty — add models to enable routing)":i.map(o=>f(o.ref)).join(" · ");return{title:`${t} (${String(i.length)})`,value:t,description:n}})}function oe(e){return e.map(t=>({title:`${t.providerID}/${t.modelID}`,description:t.name,value:t}))}function re(e){const t=[{title:"(model default)",value:{variant:void 0}}];for(const i of e)t.push({title:`@${i}`,value:{variant:i}});return t}function se(e,t){return[...e.filter((i,n)=>n===t),...e.filter((i,n)=>n!==t)]}function ae(e,t,i){const n=[...e],o=t+i,r=n[t],s=n[o];return r===void 0||s===void 0||(n[t]=s,n[o]=r),n}function ce(e,t){return e.filter((i,n)=>n!==t)}function ue(e){const t=[];for(const i of w)for(const n of e.tiers[i])t.push({tier:i,status:n});return t}function j(e){const t=new Date(e),i=String(t.getHours()).padStart(2,"0"),n=String(t.getMinutes()).padStart(2,"0"),o=String(t.getMonth()+1).padStart(2,"0"),r=String(t.getDate()).padStart(2,"0");return`${String(t.getFullYear())}-${o}-${r} ${i}:${n}`}function de(e,t){return t===null?!1:t.providerID===e.providerID&&t.id===e.id&&t.variant===e.variant}function le(e,t){let i="";return e.windowed&&(i+=e.windowUntil===null?", windowed (deadline unknown)":`, windowed until ${j(e.windowUntil)}`),e.usedPercent!==null&&(i+=`, used ${String(e.usedPercent)}%`,e.resetAt!==null&&(i+=`, resets ${j(e.resetAt)}`)),e.breaker==="open"?i+=", breaker open":e.breaker==="half_open"&&(i+=", probing"),de(e.ref,t)&&(i+=" (pinned)"),i}function pe(e,t){return le(e,t).replace(/^[, ]+/,"")||"-"}function ve(e){const t=["MODEL","TIER","HEALTH","STATE"],i=ue(e).map(({tier:r,status:s})=>[f(s.ref),r,s.health,pe(s,e.pin)]),n=t.map((r,s)=>Math.max(r.length,...i.map(c=>c[s].length))),o=r=>r.map((s,c)=>s.padEnd(n[c])).join(" ").trimEnd();return[o(t),...i.map(o)]}const fe="No models configured yet — open Models to add some.",ge=["status","models","mode","tier","pin","receipts"];function he(e){if(e===void 0)return"how every configured model is doing";const t=w.reduce((o,r)=>o+e.tiers[r].length,0);if(t===0)return"no models configured yet";const i=w.reduce((o,r)=>o+e.tiers[r].filter(s=>s.windowed).length,0),n=i===0?"":` · ${String(i)} windowed`;return`${String(t)} resource${t===1?"":"s"}${n}`}function me(e){return w.map(i=>`${i}: ${String(e.tiers[i].length)}`).join(" · ")}function we(e){return e===void 0?"pin a tier for the current session":e===null?"not pinned — the routing ladder decides":`pinned to ${e} — steers this session`}function be(e,t){if(e===t.override)return"override";if(e===t.effective)return"current"}function $e(e,t,i,n=e.mode){const o=r=>{switch(r){case"status":return{title:"Status",value:r,description:he(t)};case"models":return{title:"Models",value:r,description:me(e)};case"mode":return{title:"Mode",value:r,description:`currently ${n}`};case"tier":return{title:"Tier",value:r,description:we(i)};case"pin":return{title:"Pin",value:r,description:e.pin===null?"force one resource":`pinned to ${f(e.pin)}`};case"receipts":return{title:"Receipts",value:r,description:"why each turn ran the model it ran"}}};return ge.map(o)}function Se(e){return E.map(t=>({title:t===e?`${t} (current)`:t,value:t,description:V(t)}))}function V(e){switch(e){case"off":return"Adaptive does nothing";case"observe":return"Record decisions without applying them";case"active":return"Apply routing and recovery decisions"}}function ye(e,t){return[{title:"Global mode…",description:`applies to every session without an override (now ${e})`,value:"global"},{title:"This session…",description:`override for this session only (now ${t})`,value:"session"}]}function ke(e){const t=[{title:`Follow global (${e.global})`,description:"clear this session's override",value:{kind:"global"}}];for(const i of E){const n=be(i,e);t.push({title:n===void 0?i:`${i} (${n})`,value:{kind:"mode",mode:i},description:V(i)})}return t}function Ie(e){switch(e){case"quick":return"routine edits, mechanical steps";case"standard":return"the default";case"advanced":return"multi-step design, broad refactors"}}function Te(e){const t=[{title:"Follow routing ladder",description:"clear this session's tier request",value:{kind:"ladder"}}];for(const i of w)t.push({title:i===e.pinned?`${i} (pinned)`:i,value:{kind:"tier",tier:i},description:Ie(i)});return t}function F(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){const t=e.message;return typeof t=="string"&&t.length>0?t:JSON.stringify(e,(i,n)=>typeof n=="bigint"?String(n):n)}return String(e)}function C(e){switch(e.kind){case"apply":{const t=e.tier===void 0?"":` [${e.tier}]`;return`apply ${f(e.model)}${t} (${e.reason})`}case"defer":{const t=e.detail===void 0?"":` — ${e.detail}`;return`defer: ${e.reason}${t}`}case"park":{const t=e.detail===void 0?"":` — ${e.detail}`;return`park: ${e.reason}${t}`}case"escalate":return`escalate ${e.step} → ${f(e.target)} [${e.tier}]`;case"observe-only":return`observe: ${C(e.would)}`}}function G(e){return e===void 0?"pending":e.status==="succeeded"?"succeeded":e.status==="failed"?`failed (${e.category})`:`interrupted (${e.reason})`}function H(e){return`#${String(e.id)} ${C(e.decision)} — ${G(e.outcome)}`}function Ae(e,t=Date.now()){return[`id: ${String(e.id)}`,`when: ${De(e.at,t)} (${new Date(e.at).toISOString()})`,`session: ${e.sessionID}`,`unit: ${e.unitID??"-"}`,`tier: ${e.tier??"-"}`,`decision: ${C(e.decision)}`,`outcome: ${G(e.outcome)}`,"",`What happened: ${Me(e)}`].join(`
2
+ `)}function De(e,t){const i=Math.max(0,Math.floor((t-e)/1e3));if(i<60)return"just now";const n=Math.floor(i/60);if(n<60)return`${String(n)}m ago`;const o=Math.floor(n/60);return o<24?`${String(o)}h ago`:`${String(Math.floor(o/24))}d ago`}function Me(e){const t=e.decision;switch(t.kind){case"apply":return _e[t.reason];case"defer":return Ee[t.reason];case"park":return Re[t.reason];case"escalate":return`This turn needed stronger capability, so Adaptive moved it to ${f(t.target)}.`;case"observe-only":return"Adaptive is observing: this is what it would have done, without applying it."}}const _e={pin:"Adaptive routed to the resource you pinned.",preferred:"Adaptive chose this resource: it is the preferred resource for the tier and was available.",fallback:"The preferred resource was unavailable, so Adaptive moved the turn to the next resource in the tier.",probe:"Adaptive sent this turn as a recovery probe after an outage, to check whether the resource works again.",draining:"Adaptive routed to this resource while its provider was draining capacity."},Ee={"routing-defer":"No routing rule applied to this turn, so Adaptive left the choice to OpenCode.","adaptive-off":"Adaptive is switched off, so every turn runs on OpenCode's own choice.","pin-unverifiable":"The pinned resource could not be verified as available, so Adaptive left the choice to OpenCode.","pin-incompatible":"The pinned resource does not fit this turn's requirements, so Adaptive left the choice to OpenCode.","escalation-not-justified":"No structured capability evidence justified escalation, so Adaptive kept the current capability.","escalation-budget-exhausted":"This unit has used its escalation budget; no further escalation is allowed.","escalation-ceiling":"Adaptive is already at the highest capability tier; no stronger tier is configured.","escalation-no-candidate":"No stronger resource was eligible to escalate to (unavailable, blocked, or not configured)."},Re={"all-blocked":"Every resource in the tier is currently unavailable, so the turn ran without Adaptive routing.","no-candidates":"There are no models configured for this tier, so the turn ran without Adaptive routing.","no-compatible-resource":"No configured resource fits this turn's requirements, so the turn ran without Adaptive routing.","pin-blocked":"The pinned resource is unavailable and no fallback is allowed while pinned."},_=20;function Oe(e){return e.slice(0,_)}const P="OpenCode Adaptive",Ce="Adaptive starts in Observe mode: routing decisions are recorded as receipts, but no model is switched. Open /adaptive to configure tiers or switch to Active.";async function b(e,t){try{return await t()}catch(i){e.view.toast(F(i),"error");return}}async function y(e,t,i){const n=await b(e,()=>e.api.setSettings(t));if(n!==void 0){if(!n.ok){await e.view.alert({title:"Invalid settings",message:n.errors.join("; ")});return}return i.once||(e.view.toast("Settings saved","success"),i.once=!0),n.settings}}async function q(e,t,i,n){const o=await b(e,()=>e.api.setSessionMode(t,i));if(o!==void 0){if(!o.ok){await e.view.alert({title:"Invalid settings",message:o.errors.join("; ")});return}return n.once||(e.view.toast("Settings saved","success"),n.once=!0),o}}async function je(e,t,i,n){const o=await b(e,()=>e.api.setTierRequest(t,i));if(o!==void 0){if(!o.ok){await e.view.alert({title:"Invalid tier request",message:o.errors.join("; ")});return}return n.once||(e.view.toast("Settings saved","success"),n.once=!0),o}}async function Pe(e){const t=await b(e,()=>e.api.settings());if(t===void 0)return;let i=t;const n={once:!1},o=e.sessionID;let r=o===void 0?null:await b(e,()=>e.api.sessionMode(o))??null;if(!i.inviteDismissed){await e.view.confirm({title:P,message:Ce});const s=await e.view.select({title:P,options:[{title:"Open the menu now",value:!0},{title:"Later",value:!1}]}),c=await y(e,{inviteDismissed:!0},n);if(c!==void 0&&(i=c),s!==!0)return}for(;;){const[s,c]=await Promise.all([e.api.settings().catch(()=>{}),e.api.status().catch(()=>{})]);s!==void 0&&(i=s);const p=o===void 0?void 0:await e.api.tierRequest(o).catch(()=>{}),d=await e.view.select({title:`Adaptive (${r??i.mode})`,options:$e(i,c,p,r??i.mode)});if(d===void 0)return;if(d==="status"){await xe(e);continue}if(d==="models"){i=await Le(e,n)??i;continue}if(d==="mode"){const g=await qe(e,i,n,o,r);g!==void 0&&(i=g.settings,r=g.override);continue}if(d==="tier"){if(o===void 0){await e.view.alert({title:"This session",message:"This session's tier request can only be changed inside a session. Open a session and run /adaptive from there."});continue}await Ne(e,o,n);continue}if(d==="pin"){i=await Ue(e,i,n)??i;continue}await We(e)}}async function qe(e,t,i,n,o){for(;;){const r=await e.view.select({title:"Operating mode",options:ye(t.mode,o??t.mode)});if(r===void 0)return;if(r==="global"){const p=await e.view.select({title:"Global mode",options:Se(t.mode)});if(p===void 0||p===t.mode)return;const d=await y(e,{mode:p},i);return d===void 0?void 0:{settings:d,override:o}}if(n===void 0){await e.view.alert({title:"This session",message:"This session's mode can only be changed inside a session. Open a session and run /adaptive from there."});continue}const s=await e.view.select({title:"This session's mode",options:ke({global:t.mode,override:o,effective:o??t.mode})});return s===void 0?void 0:s.kind==="global"?o===null||await q(e,n,null,i)===void 0?void 0:{settings:t,override:null}:s.mode===(o??t.mode)||await q(e,n,s.mode,i)===void 0?void 0:{settings:t,override:s.mode}}}async function Ne(e,t,i){const n=await b(e,()=>e.api.tierRequest(t));if(n===void 0)return;const o=await e.view.select({title:"This session's tier",options:Te({pinned:n})});if(o===void 0)return;const r=o.kind==="ladder"?null:o.tier;r!==n&&await je(e,t,r,i)}async function xe(e){const t=await b(e,()=>e.api.status());if(t===void 0)return;if(!w.some(n=>t.tiers[n].length>0)){await e.view.alert({title:"Status",message:fe});return}await e.view.table({title:"Status",lines:ve(t)})}async function Le(e,t){let i;for(;;){const n=await b(e,()=>e.api.settings());if(n===void 0)return i;const o=await b(e,()=>e.api.status());if(o===void 0)return i;const r=await e.view.select({title:"Tier",options:ne(o)});if(r===void 0)return i;let s=o,c=n;for(;;){const p=await e.view.select({title:`Tier: ${r}`,options:ie(c,r,s)});if(p===void 0)break;if(p.kind==="add"){const u=await Ve(e,c,r,t);u!==void 0&&(i=u,c=u)}else{const u=await Ge(e,c,r,p,t);u!==void 0&&(i=u,c=u)}const[d,g]=await Promise.all([e.api.settings().catch(()=>{}),e.api.status().catch(()=>{})]);d!==void 0&&(i=d,c=d),g!==void 0&&(s=g)}}}async function Ve(e,t,i,n){const o=e.models(),r=await e.view.select({title:"Add model",placeholder:"Search provider / model / name",options:oe(o)});if(r===void 0)return;const s=r.variants;let c;if(s.length>0){const g=await e.view.select({title:`Variant: ${r.providerID}/${r.modelID}`,options:re(s)});if(g===void 0)return;c=g.variant}const p=c===void 0?{providerID:r.providerID,id:r.modelID}:{providerID:r.providerID,id:r.modelID,variant:c},d=[...t.tiers[i],p];return y(e,{tiers:{...t.tiers,[i]:d}},n)}function Fe(){return[{title:"Move to top",value:"top",description:"Make this the primary resource"},{title:"Move up",value:"up",description:"Higher priority within the tier"},{title:"Move down",value:"down",description:"Lower priority within the tier"},{title:"Remove",value:"remove"}]}async function Ge(e,t,i,{index:n,ref:o},r){const s=await e.view.select({title:`Resource: ${f(o)}`,options:Fe()});if(s===void 0)return;if(s==="remove")return await e.view.confirm({title:"Remove resource",message:`Remove ${f(o)} from ${i}?`})!==!0?void 0:y(e,{tiers:{...t.tiers,[i]:ce(t.tiers[i],n)}},r);const c=s==="top"?se(t.tiers[i],n):ae(t.tiers[i],n,s==="up"?-1:1);return y(e,{tiers:{...t.tiers,[i]:c}},r)}function He(e){const t=[{title:"No pin",value:{kind:"clear"},description:"clear the pin"}];for(const i of w)for(const n of e.tiers[i])t.push({title:`${f(n)} (${i})`,description:"route every turn to this resource, even in active mode",value:{kind:"pin",ref:n}});return t}async function Ue(e,t,i){if(!w.some(r=>t.tiers[r].length>0)){await e.view.alert({title:"Pin",message:"(no configured resources to pin)"});return}const o=await e.view.select({title:"Pin",options:He(t)});if(o!==void 0)return y(e,{pin:o.kind==="clear"?null:o.ref},i)}async function We(e){const t=await b(e,()=>e.api.receipts());if(t!==void 0){if(t.length===0){await e.view.alert({title:"Receipts",message:"No decisions recorded yet."});return}for(let i=0;;){const n=Oe(t.slice(i)),o=i+_<t.length,r=await e.view.selectPaged({title:`Receipts (${String(t.length)}, newest first)`,options:n.map(c=>({title:H(c),value:c})),actions:[...i>0?[{title:"Previous page",value:"prev",bind:"ctrl+b"}]:[],...o?[{title:"Next page",value:"next",bind:"ctrl+f"}]:[]]});if(r.action==="next"){i+=_;continue}if(r.action==="prev"){i=Math.max(0,i-_);continue}const s=r.picked;if(s===void 0)return;await e.view.alert({title:`Receipt #${String(s.id)}`,message:Ae(s)})}}}var ze=a.template("<pre class=adaptive-table>");function Je(e){return m.onCleanup(e.done),a.createComponent(e.host.ui.Dialog,{get title(){return e.title},size:"x-large",get footer(){return a.createComponent(e.host.ui.Button,{get onClick(){return e.close},children:"Close"})},get children(){var t=ze();return a.insert(t,()=>e.lines.join(`
3
+ `)),t}})}function Ye(e){return{select:({title:t,placeholder:i,options:n})=>e.dialogs.select({title:t,options:n,...i!==void 0?{placeholder:i}:{}}),selectPaged:async t=>{const i=await e.dialogs.select({title:t.title,options:[...t.options.map(n=>({title:n.title,value:{kind:"pick",value:n.value}})),...t.actions.map(n=>({title:n.title,value:{kind:"action",value:n.value}}))]});return i===void 0?{picked:void 0,action:void 0}:i.kind==="pick"?{picked:i.value,action:void 0}:{picked:void 0,action:i.value}},confirm:({title:t,message:i})=>e.dialogs.confirm({title:t,description:i}),alert:({title:t,message:i})=>e.dialogs.alert({title:t,description:i}),table:({title:t,lines:i})=>new Promise(n=>{e.dialogs.open(o=>a.createComponent(Je,{host:e,title:t,lines:i,close:()=>{o.close()},done:n}))}),toast:(t,i)=>{e.toast.show({title:t,variant:i})}}}function U(e){const t=e.sessions.current();return(t==null?void 0:t.server.builtin)===!0?t:void 0}function W(e){var i,n;const t=(n=(i=U(e))==null?void 0:i.location)==null?void 0:n.directory;return t===void 0?{}:{location:{directory:t}}}function Be(e){const t=e.client.rpc(x),i=()=>W(e);return{settings:()=>t["settings.get"](void 0,i()),setSettings:n=>t["settings.set"](n,i()),status:()=>t["status.get"](void 0,i()),receipts:()=>t["receipts.list"](void 0,i()),sessionMode:n=>t["session-mode.get"]({sessionID:n},i()).then(o=>o.mode),setSessionMode:(n,o)=>t["session-mode.set"]({sessionID:n,mode:o},i()),tierRequest:n=>t["tier-request.get"]({sessionID:n},i()).then(o=>o.tier),setTierRequest:(n,o)=>t["tier-request.set"]({sessionID:n,tier:o},i())}}function Xe(e){const t=e.servers.list().map(n=>e.servers.get(n)).find(n=>(n==null?void 0:n.builtin)===!0);return t===void 0?void 0:t.data.location.model}async function Ze(e){const t=Ye(e),i=U(e),n=Xe(e);await(async()=>{await(n==null?void 0:n.sync(i==null?void 0:i.location).catch(()=>{})),await Pe({api:Be(e),view:t,models:()=>((n==null?void 0:n.list(i==null?void 0:i.location))??[]).map(r=>({providerID:r.providerID,modelID:r.modelID,name:r.name,variants:r.variants.map(s=>s.id)})),...i!==void 0?{sessionID:i.id}:{}})})().catch(r=>{t.toast(F(r),"error")})}const Ke="connection";function Qe(e){if(e===void 0)return[{id:Ke,label:"Server",value:"disconnected",tone:"warning"}];const t=[{id:"mode",label:"Mode",value:e.mode},{id:"pin",label:"Pin",value:e.pin===null?"none":f(e.pin)}];for(const i of w){const n=e.tiers[i],o=n.length===0?"empty":n.map(r=>`${f(r.ref)}${L(r)}`).join(", ");t.push({id:`tier-${i}`,label:`Tier ${i}`,value:o})}return t.push({id:"parked",label:"Parked",value:String(e.parked)}),t.push({id:"receipts",label:"Receipts",value:String(e.receipts)}),t}function et(e){return E.map(t=>({value:t,title:t===e?`${t} (current)`:t}))}function tt(e){return E.find(t=>t===e)}function it(e){return e.map(t=>H(t))}var nt=a.template("<span class=adaptive-saved role=status>Saved"),ot=a.template("<div class=adaptive-receipts>"),rt=a.template('<div class=adaptive-page><table class=adaptive-status aria-label="OpenCode Adaptive status"><tbody></tbody></table><label class=adaptive-mode><span class=adaptive-row-label>Mode</span><select class=adaptive-mode-select>'),st=a.template("<tr class=adaptive-row><td class=adaptive-row-label></td><td class=adaptive-row-value>"),at=a.template("<option>"),ct=a.template("<div class=adaptive-receipt-line>");const ut=5;function z(e){const[t,i]=m.createSignal(void 0),[n,o]=m.createSignal("observe"),[r,s]=m.createSignal([]),[c,p]=m.createSignal(!1),d=()=>{e.api.status().then(u=>{i(u),u!==void 0&&o(u.mode)}),e.api.receipts().then(u=>{s(u??[])})};m.onCleanup(e.api.watchSettings(d)),d();const g=u=>{const k=tt(u.value);u.value=n(),k!==void 0&&e.api.update({mode:k}).then(R=>{p(R),d()})};return(()=>{var u=rt(),k=u.firstChild,R=k.firstChild,J=k.nextSibling,Y=J.firstChild,A=Y.nextSibling;return a.insert(R,a.createComponent(m.For,{get each(){return Qe(t())},children:h=>(()=>{var $=st(),I=$.firstChild,B=I.nextSibling;return a.insert(I,()=>h.label),a.insert(B,()=>h.value),a.effect(()=>a.setAttribute($,"data-tone",h.tone)),$})()})),A.addEventListener("change",h=>{g(h.currentTarget)}),a.insert(A,a.createComponent(m.For,{get each(){return et(n())},children:h=>(()=>{var $=at();return a.insert($,()=>h.title),a.effect(()=>$.value=h.value),$})()})),a.insert(u,a.createComponent(m.Show,{get when(){return c()},get children(){return nt()}}),null),a.insert(u,a.createComponent(m.Show,{get when(){return t()!==void 0},get children(){var h=ot();return a.insert(h,a.createComponent(m.For,{get each(){return it(r()).slice(0,ut)},children:$=>(()=>{var I=ct();return a.insert(I,$),I})()})),h}}),null),a.effect(()=>A.disabled=t()===void 0),a.effect(()=>A.value=n()),u})()}const dt=O.Plugin.define({setup(e){const t=te(e.client,()=>W(e));e.add(O.SettingsPage,{id:"adaptive",title:"OpenCode Adaptive",entries:[{id:"adaptive",title:"OpenCode Adaptive",description:"Execution tier routing, operating mode and resource health"}],render:()=>a.createComponent(z,{api:t})}),e.add(O.Command,{id:"open",title:"OpenCode Adaptive",group:"Adaptive",suggested:!0,slash:{name:"adaptive"},run:()=>Ze(e)})}});exports.AdaptiveStatusPage=z;exports.default=dt;
package/dist/gui.d.ts CHANGED
@@ -10,7 +10,8 @@ export declare function AdaptiveStatusPage(props: {
10
10
  }): JSX.Element;
11
11
  /**
12
12
  * GUI plugin entry (the `./gui-experimental` bundle default export). Thin adapter: registers a native
13
- * SettingsPage and a palette command that opens it; no routing logic lives here (ADP-034/035).
13
+ * SettingsPage and a palette/`/adaptive` slash command that runs the same dialog flow as the TUI (ui/flow); no
14
+ * routing logic lives here (ADP-034/035).
14
15
  */
15
16
  declare const _default: Plugin.Definition<{}>;
16
17
  export default _default;
package/dist/gui.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "solid-js/jsx-runtime";
2
2
  import { Command, Plugin, SettingsPage } from "@opencode/plugin/gui";
3
3
  import { For, Show, createSignal, onCleanup } from "solid-js";
4
4
  import { adaptiveGuiApi } from "./ui/gui/api.js";
5
+ import { openAdaptive, rpcLocation } from "./ui/gui/flow.js";
5
6
  import { modeChoices, parseModeValue, receiptLines, statusRows } from "./ui/gui/view.js";
6
7
  const RECEIPTS_PREVIEW_LIMIT = 5;
7
8
  /**
@@ -43,11 +44,12 @@ export function AdaptiveStatusPage(props) {
43
44
  }
44
45
  /**
45
46
  * GUI plugin entry (the `./gui-experimental` bundle default export). Thin adapter: registers a native
46
- * SettingsPage and a palette command that opens it; no routing logic lives here (ADP-034/035).
47
+ * SettingsPage and a palette/`/adaptive` slash command that runs the same dialog flow as the TUI (ui/flow); no
48
+ * routing logic lives here (ADP-034/035).
47
49
  */
48
50
  export default Plugin.define({
49
51
  setup(context) {
50
- const api = adaptiveGuiApi(context.client);
52
+ const api = adaptiveGuiApi(context.client, () => rpcLocation(context));
51
53
  context.add(SettingsPage, {
52
54
  id: "adaptive",
53
55
  title: "OpenCode Adaptive",
@@ -64,9 +66,9 @@ export default Plugin.define({
64
66
  id: "open",
65
67
  title: "OpenCode Adaptive",
66
68
  group: "Adaptive",
67
- run: () => {
68
- context.layout.settings("adaptive");
69
- },
69
+ suggested: true,
70
+ slash: { name: "adaptive" },
71
+ run: () => openAdaptive(context),
70
72
  });
71
73
  },
72
74
  });
package/dist/gui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"gui.js","sourceRoot":"","sources":["../src/gui.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Command, Plugin, SettingsPage, type Context } from \"@opencode/plugin/gui\"\nimport { For, Show, createSignal, onCleanup } from \"solid-js\"\nimport type { JSX } from \"solid-js\"\n\nimport type { OperatingMode } from \"./core/scheduler/types.js\"\nimport type { Receipt } from \"./state/receipts/index.js\"\nimport type { StatusSnapshot } from \"./rpc.js\"\nimport { adaptiveGuiApi, type GuiApi } from \"./ui/gui/api.js\"\nimport { modeChoices, parseModeValue, receiptLines, statusRows } from \"./ui/gui/view.js\"\n\nconst RECEIPTS_PREVIEW_LIMIT = 5\n\n/**\n * Native GUI settings page body (ADP-034). All data comes from the typed RPC adapter; the component holds no\n * local settings state. A failed read renders the disconnected row instead of stale data.\n */\nexport function AdaptiveStatusPage(props: { api: GuiApi }): JSX.Element {\n const [snapshot, setSnapshot] = createSignal<StatusSnapshot | undefined>(undefined)\n const [mode, setMode] = createSignal<OperatingMode>(\"observe\")\n const [receipts, setReceipts] = createSignal<Receipt[]>([])\n const [saved, setSaved] = createSignal(false)\n\n const refresh = (): void => {\n void props.api.status().then((next) => {\n setSnapshot(next)\n if (next !== undefined) setMode(next.mode)\n })\n void props.api.receipts().then((next) => {\n setReceipts(next ?? [])\n })\n }\n\n onCleanup(props.api.watchSettings(refresh))\n refresh()\n\n const pick = (select: HTMLSelectElement): void => {\n const value = parseModeValue(select.value)\n // The DOM changes before onChange, even if the signal stays unchanged\n // after a rejected write. Keep the control on the confirmed server value.\n select.value = mode()\n if (value === undefined) return\n void props.api.update({ mode: value }).then((ok) => {\n setSaved(ok)\n refresh()\n })\n }\n\n return (\n <div class=\"adaptive-page\">\n <table class=\"adaptive-status\" aria-label=\"OpenCode Adaptive status\">\n <tbody>\n <For each={statusRows(snapshot())}>\n {(row) => (\n <tr class=\"adaptive-row\" data-tone={row.tone}>\n <td class=\"adaptive-row-label\">{row.label}</td>\n <td class=\"adaptive-row-value\">{row.value}</td>\n </tr>\n )}\n </For>\n </tbody>\n </table>\n <label class=\"adaptive-mode\">\n <span class=\"adaptive-row-label\">Mode</span>\n <select\n class=\"adaptive-mode-select\"\n disabled={snapshot() === undefined}\n value={mode()}\n onChange={(event) => {\n pick(event.currentTarget)\n }}\n >\n <For each={modeChoices(mode())}>\n {(choice) => <option value={choice.value}>{choice.title}</option>}\n </For>\n </select>\n </label>\n <Show when={saved()}>\n <span class=\"adaptive-saved\" role=\"status\">\n Saved\n </span>\n </Show>\n <Show when={snapshot() !== undefined}>\n <div class=\"adaptive-receipts\">\n <For each={receiptLines(receipts()).slice(0, RECEIPTS_PREVIEW_LIMIT)}>\n {(line) => <div class=\"adaptive-receipt-line\">{line}</div>}\n </For>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * GUI plugin entry (the `./gui-experimental` bundle default export). Thin adapter: registers a native\n * SettingsPage and a palette command that opens it; no routing logic lives here (ADP-034/035).\n */\nexport default Plugin.define({\n setup(context: Context): void {\n const api = adaptiveGuiApi(context.client)\n context.add(SettingsPage, {\n id: \"adaptive\",\n title: \"OpenCode Adaptive\",\n entries: [\n {\n id: \"adaptive\",\n title: \"OpenCode Adaptive\",\n description: \"Execution tier routing, operating mode and resource health\",\n },\n ],\n render: () => <AdaptiveStatusPage api={api} />,\n })\n context.add(Command, {\n id: \"open\",\n title: \"OpenCode Adaptive\",\n group: \"Adaptive\",\n run: () => {\n context.layout.settings(\"adaptive\")\n },\n })\n },\n})\n"]}
1
+ {"version":3,"file":"gui.js","sourceRoot":"","sources":["../src/gui.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Command, Plugin, SettingsPage, type Context } from \"@opencode/plugin/gui\"\nimport { For, Show, createSignal, onCleanup } from \"solid-js\"\nimport type { JSX } from \"solid-js\"\n\nimport type { OperatingMode } from \"./core/scheduler/types.js\"\nimport type { Receipt } from \"./state/receipts/index.js\"\nimport type { StatusSnapshot } from \"./rpc.js\"\nimport { adaptiveGuiApi, type GuiApi } from \"./ui/gui/api.js\"\nimport { openAdaptive, rpcLocation } from \"./ui/gui/flow.js\"\nimport { modeChoices, parseModeValue, receiptLines, statusRows } from \"./ui/gui/view.js\"\n\nconst RECEIPTS_PREVIEW_LIMIT = 5\n\n/**\n * Native GUI settings page body (ADP-034). All data comes from the typed RPC adapter; the component holds no\n * local settings state. A failed read renders the disconnected row instead of stale data.\n */\nexport function AdaptiveStatusPage(props: { api: GuiApi }): JSX.Element {\n const [snapshot, setSnapshot] = createSignal<StatusSnapshot | undefined>(undefined)\n const [mode, setMode] = createSignal<OperatingMode>(\"observe\")\n const [receipts, setReceipts] = createSignal<Receipt[]>([])\n const [saved, setSaved] = createSignal(false)\n\n const refresh = (): void => {\n void props.api.status().then((next) => {\n setSnapshot(next)\n if (next !== undefined) setMode(next.mode)\n })\n void props.api.receipts().then((next) => {\n setReceipts(next ?? [])\n })\n }\n\n onCleanup(props.api.watchSettings(refresh))\n refresh()\n\n const pick = (select: HTMLSelectElement): void => {\n const value = parseModeValue(select.value)\n // The DOM changes before onChange, even if the signal stays unchanged\n // after a rejected write. Keep the control on the confirmed server value.\n select.value = mode()\n if (value === undefined) return\n void props.api.update({ mode: value }).then((ok) => {\n setSaved(ok)\n refresh()\n })\n }\n\n return (\n <div class=\"adaptive-page\">\n <table class=\"adaptive-status\" aria-label=\"OpenCode Adaptive status\">\n <tbody>\n <For each={statusRows(snapshot())}>\n {(row) => (\n <tr class=\"adaptive-row\" data-tone={row.tone}>\n <td class=\"adaptive-row-label\">{row.label}</td>\n <td class=\"adaptive-row-value\">{row.value}</td>\n </tr>\n )}\n </For>\n </tbody>\n </table>\n <label class=\"adaptive-mode\">\n <span class=\"adaptive-row-label\">Mode</span>\n <select\n class=\"adaptive-mode-select\"\n disabled={snapshot() === undefined}\n value={mode()}\n onChange={(event) => {\n pick(event.currentTarget)\n }}\n >\n <For each={modeChoices(mode())}>\n {(choice) => <option value={choice.value}>{choice.title}</option>}\n </For>\n </select>\n </label>\n <Show when={saved()}>\n <span class=\"adaptive-saved\" role=\"status\">\n Saved\n </span>\n </Show>\n <Show when={snapshot() !== undefined}>\n <div class=\"adaptive-receipts\">\n <For each={receiptLines(receipts()).slice(0, RECEIPTS_PREVIEW_LIMIT)}>\n {(line) => <div class=\"adaptive-receipt-line\">{line}</div>}\n </For>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * GUI plugin entry (the `./gui-experimental` bundle default export). Thin adapter: registers a native\n * SettingsPage and a palette/`/adaptive` slash command that runs the same dialog flow as the TUI (ui/flow); no\n * routing logic lives here (ADP-034/035).\n */\nexport default Plugin.define({\n setup(context: Context): void {\n const api = adaptiveGuiApi(context.client, () => rpcLocation(context))\n context.add(SettingsPage, {\n id: \"adaptive\",\n title: \"OpenCode Adaptive\",\n entries: [\n {\n id: \"adaptive\",\n title: \"OpenCode Adaptive\",\n description: \"Execution tier routing, operating mode and resource health\",\n },\n ],\n render: () => <AdaptiveStatusPage api={api} />,\n })\n context.add(Command, {\n id: \"open\",\n title: \"OpenCode Adaptive\",\n group: \"Adaptive\",\n suggested: true,\n slash: { name: \"adaptive\" },\n run: () => openAdaptive(context),\n })\n },\n})\n"]}
@@ -7,6 +7,14 @@ const TEXT_BOUND = 512;
7
7
  // These messages describe the subscription's shared allowance or balance,
8
8
  // unlike a generic quota error or a named model-family limit (ADP-007).
9
9
  const SHARED_QUOTA_PATTERN = /\byou['’]ve hit your (?:session|usage|weekly) limit(?=\s*(?:$|[·.,;(:]|resets?\b))|\b(?:out of credits|insufficient credits|credit balance)\b/i;
10
+ // A 429 whose message says the quota itself is spent ("You exceeded your
11
+ // current quota, please check your plan and billing details") is quota
12
+ // exhaustion: a burst-sized window would re-admit it every half minute. The
13
+ // status stays the definitive evidence; text only picks the window. Messages
14
+ // that also speak of rate limits keep the short window — Gemini words its
15
+ // per-minute 429s as "exceeded your current quota" and links its rate-limit
16
+ // docs.
17
+ const RATE_LIMIT_TEXT = /rate[ _-]?limit|per min/i;
10
18
  /**
11
19
  * Retry/failover policy per category (ADP-010, ADP-011).
12
20
  *
@@ -232,7 +240,15 @@ export function classifyFailure(evidence) {
232
240
  if (isHttpStatus(evidence.status)) {
233
241
  const category = classifyStatus(evidence.status, headers);
234
242
  if (category !== undefined) {
235
- return classification(category, "status", true, structuredRetryAfterMs ?? retryAfterHeaderMs(headers));
243
+ const retryAfterMs = structuredRetryAfterMs ?? retryAfterHeaderMs(headers);
244
+ if (category === "rate_limited" && evidence.text !== undefined) {
245
+ const bounded = evidence.text.slice(0, TEXT_BOUND);
246
+ if (classifyText(bounded) === "quota_exhausted" && !RATE_LIMIT_TEXT.test(bounded)) {
247
+ const quotaScope = SHARED_QUOTA_PATTERN.test(bounded) ? "provider" : undefined;
248
+ return classification("quota_exhausted", "status", true, retryAfterMs, quotaScope);
249
+ }
250
+ }
251
+ return classification(category, "status", true, retryAfterMs);
236
252
  }
237
253
  }
238
254
  // Layer 3: standard headers alone (Retry-After without a decisive status).
@@ -1 +1 @@
1
- {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../../src/health/classifier/classify.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ErrorEvidence, FailureCategory, FailureClassification, RetryAdvice } from \"./types.js\"\n\n/**\n * Text fed to the textual last-resort layer is truncated to this bound before\n * matching, so adversarial or enormous error bodies cannot dominate\n * classification cost or smuggle patterns in late bytes (ADP-008).\n */\nconst TEXT_BOUND = 512\n\n// These messages describe the subscription's shared allowance or balance,\n// unlike a generic quota error or a named model-family limit (ADP-007).\nconst SHARED_QUOTA_PATTERN =\n /\\byou['’]ve hit your (?:session|usage|weekly) limit(?=\\s*(?:$|[·.,;(:]|resets?\\b))|\\b(?:out of credits|insufficient credits|credit balance)\\b/i\n\n/**\n * Retry/failover policy per category (ADP-010, ADP-011).\n *\n * - Infrastructure failures (rate limiting, quota, outage, overload, timeout)\n * are failover-eligible: horizontal recovery within the tier is exactly\n * their remedy.\n * - Permanent configuration failures (auth, invalid model, content policy)\n * and context overflow never retry and never failover: retrying the same\n * request cannot help and switching resources would reproduce the failure.\n * - User cancellation is final: no retry, no failover (ADP-010).\n * - Unknown failures defer to OpenCode's native handling.\n */\nconst CATEGORY_POLICY: Readonly<\n Record<FailureCategory, { retryable: boolean; failoverEligible: boolean }>\n> = {\n rate_limited: { retryable: true, failoverEligible: true },\n quota_exhausted: { retryable: false, failoverEligible: true },\n outage: { retryable: true, failoverEligible: true },\n overload: { retryable: true, failoverEligible: true },\n timeout: { retryable: true, failoverEligible: true },\n auth_failure: { retryable: false, failoverEligible: false },\n invalid_model: { retryable: false, failoverEligible: false },\n context_overflow: { retryable: false, failoverEligible: false },\n content_policy: { retryable: false, failoverEligible: false },\n user_cancelled: { retryable: false, failoverEligible: false },\n unknown: { retryable: false, failoverEligible: false },\n}\n\n/**\n * Error tags from the documented @opencode/protocol vocabulary that map\n * directly to a failure category. Tags absent here (`ProviderRequestError`,\n * `UnknownError`, anything unrecognized) are structured but not\n * category-specific: they never guess, and classification falls through to\n * the refining layers.\n */\nconst STRUCTURED_TYPES = new Map<string, FailureCategory>([\n [\"UnauthorizedError\", \"auth_failure\"],\n [\"ForbiddenError\", \"auth_failure\"],\n [\"ModelNotFoundError\", \"invalid_model\"],\n [\"UnsupportedModelError\", \"invalid_model\"],\n [\"ServiceUnavailableError\", \"outage\"],\n])\n\n/**\n * Bounded textual patterns, last resort only (ADP-008). Ordered most\n * specific first; the first match wins. These encode provider error-message\n * conventions documented by the research phase, not a general-purpose\n * keyword classifier.\n */\nconst TEXTUAL_PATTERNS: readonly {\n readonly category: FailureCategory\n readonly pattern: RegExp\n}[] = [\n {\n category: \"context_overflow\",\n pattern:\n /context (length|window)|maximum context|input length exceeds|prompt is too long|too many (input )?tokens|exceeds the maximum/i,\n },\n {\n // Subscription limits (\"You've hit your session/usage limit\") are quota\n // exhaustion with a provider-side reset window; the reset time in the\n // text is not parsed (the classifier has no clock) — the health layer's\n // default quota window bounds the quarantine instead.\n // Sibling fallback plugins' issue trackers document the real-world\n // phrasings that missed curated whitelists (oh-my-openagent #2393/#5815,\n // lichter's 429 lesson): snake-cased limits, \"usage exceeded\" without\n // \"limit\", credit exhaustion without \"balance\". Qualifiers stay required\n // so \"token limit reached\" (context overflow) never matches quota.\n category: \"quota_exhausted\",\n pattern:\n /\\bquota\\b|credit balance|(?:session|usage|free usage|message|plan|monthly|weekly|daily)[ _-]?limit|usage exceeded|out of credits|insufficient credits/i,\n },\n { category: \"rate_limited\", pattern: /rate ?limit/i },\n { category: \"timeout\", pattern: /\\btimeout\\b|timed? ?out|deadline exceeded|etimedout/i },\n {\n category: \"auth_failure\",\n pattern:\n /unauthorized|invalid (api|access) key|api key (expired|invalid)|authentication|permission denied/i,\n },\n {\n category: \"invalid_model\",\n pattern: /model.{0,30}(does not exist|not found)|invalid model|unknown model|no such model/i,\n },\n {\n category: \"content_policy\",\n pattern:\n /content (management )?(policy|filter)|safety system|violates?.{0,32}policy|flagged as/i,\n },\n { category: \"overload\", pattern: /overloaded|over capacity|high demand/i },\n { category: \"user_cancelled\", pattern: /cancel+(l)?ed|aborted by (the )?user|user abort/i },\n]\n\nfunction isHttpStatus(status: number | undefined): status is number {\n return typeof status === \"number\" && Number.isInteger(status) && status >= 100 && status <= 599\n}\n\n/**\n * Upper bound on a provider-claimed retry delay. `Retry-After` and structured\n * retry metadata are untrusted provider data: a hostile or misconfigured\n * value (\"999999999\" seconds) must not convert one response into a\n * near-permanent resource blacklist. The host's own retry scheduling waits\n * out the raw header verbatim — this bound only concerns the health window\n * the supervisor derives from the classification (ADP-011 boundedness).\n */\nconst MAX_RETRY_AFTER_MS = 24 * 60 * 60 * 1000\n\nfunction boundedRetryAfterMs(retryAfterMs: number | undefined): number | undefined {\n if (retryAfterMs === undefined) {\n return undefined\n }\n return Math.min(retryAfterMs, MAX_RETRY_AFTER_MS)\n}\n\nfunction isNonNegativeInteger(value: number | undefined): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0\n}\n\nfunction lowercasedHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n): ReadonlyMap<string, string> {\n const lowered = new Map<string, string>()\n if (headers === undefined) {\n return lowered\n }\n for (const [name, value] of Object.entries(headers)) {\n lowered.set(name.toLowerCase(), value)\n }\n return lowered\n}\n\n/**\n * Parses a `Retry-After` header into milliseconds. Only the integer-seconds\n * form is accepted: the HTTP-date form requires a clock, which the classifier\n * deliberately does not have (reset-time handling belongs to the capacity\n * boundary, T-05/T-15).\n */\nfunction retryAfterHeaderMs(headers: ReadonlyMap<string, string>): number | undefined {\n const raw = headers.get(\"retry-after\")\n if (raw === undefined || !/^\\d+$/.test(raw)) {\n return undefined\n }\n return Number(raw) * 1000\n}\n\n/**\n * Standard rate-limit headers (case-insensitive `x-ratelimit-remaining*`).\n * All-zero remaining capacity indicates a fixed-window quota exhaustion\n * rather than a short burst limit.\n */\nfunction fixedWindowExhausted(headers: ReadonlyMap<string, string>): boolean {\n let sawRemaining = false\n for (const [name, value] of headers) {\n if (name.startsWith(\"x-ratelimit-remaining\")) {\n sawRemaining = true\n if (value !== \"0\") {\n return false\n }\n }\n }\n return sawRemaining\n}\n\nfunction retryAdvice(category: FailureCategory, retryAfterMs: number | undefined): RetryAdvice {\n const policy = CATEGORY_POLICY[category]\n const bounded = boundedRetryAfterMs(retryAfterMs)\n if (bounded === undefined) {\n return { retryable: policy.retryable }\n }\n return { retryable: policy.retryable, retryAfterMs: bounded }\n}\n\nfunction classification(\n category: FailureCategory,\n layer: FailureClassification[\"layer\"],\n definitive: boolean,\n retryAfterMs: number | undefined,\n quotaScope?: \"provider\",\n): FailureClassification {\n const policy = CATEGORY_POLICY[category]\n return {\n category,\n layer,\n definitive,\n retry: retryAdvice(category, retryAfterMs),\n failoverEligible: policy.failoverEligible,\n ...(quotaScope !== undefined ? { quotaScope } : {}),\n }\n}\n\n/**\n * Classifies an HTTP status. 429 consults the standard rate-limit headers in\n * the same step: status plus documented standard headers are one HTTP\n * semantics layer, and the quota refinement is more specific evidence than a\n * bare 429, so it cannot weaken the ADP-008 precedence.\n */\nfunction classifyStatus(\n status: number,\n headers: ReadonlyMap<string, string>,\n): FailureCategory | undefined {\n if (status === 429) {\n return fixedWindowExhausted(headers) ? \"quota_exhausted\" : \"rate_limited\"\n }\n if (status === 401 || status === 403) {\n return \"auth_failure\"\n }\n if (status === 408) {\n return \"timeout\"\n }\n if (status === 413) {\n return \"context_overflow\"\n }\n if (status === 502 || status === 503 || status === 504) {\n return \"outage\"\n }\n return undefined\n}\n\nfunction classifyText(text: string): FailureCategory | undefined {\n const bounded = text.length > TEXT_BOUND ? text.slice(0, TEXT_BOUND) : text\n for (const { category, pattern } of TEXTUAL_PATTERNS) {\n if (pattern.test(bounded)) {\n return category\n }\n }\n return undefined\n}\n\n/**\n * Total classification of layered failure evidence. Never throws and always\n * returns a category: any evidence shape outside the documented vocabularies\n * classifies as `unknown` via the `none` layer, deferring to OpenCode's\n * native failure handling (ADP-009, ADP-012).\n */\nexport function classifyFailure(evidence: ErrorEvidence): FailureClassification {\n // Layer 0: interruption evidence. Interruptions are host decisions, not\n // provider failures; a user cancellation is final (ADP-010).\n if (evidence.interrupted === \"user\") {\n return classification(\"user_cancelled\", \"interruption\", true, undefined)\n }\n if (evidence.interrupted !== undefined) {\n return classification(\"unknown\", \"interruption\", true, undefined)\n }\n\n // Layer 1: structured OpenCode metadata.\n const structured = evidence.structured\n const structuredRetryAfterMs = isNonNegativeInteger(structured?.retryAfterMs)\n ? structured.retryAfterMs\n : undefined\n if (structured !== undefined) {\n const mapped = STRUCTURED_TYPES.get(structured.type)\n if (mapped !== undefined) {\n return classification(mapped, \"structured\", true, structuredRetryAfterMs)\n }\n // Unmapped tag: fall through so HTTP semantics can refine the category.\n }\n\n // Layer 2: HTTP status (+ standard headers for the 429 refinement).\n const headers = lowercasedHeaders(evidence.headers)\n if (isHttpStatus(evidence.status)) {\n const category = classifyStatus(evidence.status, headers)\n if (category !== undefined) {\n return classification(\n category,\n \"status\",\n true,\n structuredRetryAfterMs ?? retryAfterHeaderMs(headers),\n )\n }\n }\n\n // Layer 3: standard headers alone (Retry-After without a decisive status).\n const headerRetryAfterMs = retryAfterHeaderMs(headers)\n if (headerRetryAfterMs !== undefined) {\n return classification(\"rate_limited\", \"headers\", false, headerRetryAfterMs)\n }\n\n // Layer 4: documented provider-specific metadata, normalized by our own\n // adapters after validation (T-15).\n if (evidence.providerMetadata?.quotaExhausted === true) {\n return classification(\"quota_exhausted\", \"provider-metadata\", true, undefined)\n }\n if (evidence.providerMetadata?.overloaded === true) {\n return classification(\"overload\", \"provider-metadata\", true, structuredRetryAfterMs)\n }\n\n // Layer 5: bounded textual classification, always non-definitive.\n const text = evidence.text\n if (text !== undefined) {\n const category = classifyText(text)\n if (category !== undefined) {\n const quotaScope =\n category === \"quota_exhausted\" && SHARED_QUOTA_PATTERN.test(text.slice(0, TEXT_BOUND))\n ? \"provider\"\n : undefined\n return classification(category, \"textual\", false, structuredRetryAfterMs, quotaScope)\n }\n }\n\n return classification(\"unknown\", \"none\", false, undefined)\n}\n"]}
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../../src/health/classifier/classify.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ErrorEvidence, FailureCategory, FailureClassification, RetryAdvice } from \"./types.js\"\n\n/**\n * Text fed to the textual last-resort layer is truncated to this bound before\n * matching, so adversarial or enormous error bodies cannot dominate\n * classification cost or smuggle patterns in late bytes (ADP-008).\n */\nconst TEXT_BOUND = 512\n\n// These messages describe the subscription's shared allowance or balance,\n// unlike a generic quota error or a named model-family limit (ADP-007).\nconst SHARED_QUOTA_PATTERN =\n /\\byou['’]ve hit your (?:session|usage|weekly) limit(?=\\s*(?:$|[·.,;(:]|resets?\\b))|\\b(?:out of credits|insufficient credits|credit balance)\\b/i\n\n// A 429 whose message says the quota itself is spent (\"You exceeded your\n// current quota, please check your plan and billing details\") is quota\n// exhaustion: a burst-sized window would re-admit it every half minute. The\n// status stays the definitive evidence; text only picks the window. Messages\n// that also speak of rate limits keep the short window — Gemini words its\n// per-minute 429s as \"exceeded your current quota\" and links its rate-limit\n// docs.\nconst RATE_LIMIT_TEXT = /rate[ _-]?limit|per min/i\n\n/**\n * Retry/failover policy per category (ADP-010, ADP-011).\n *\n * - Infrastructure failures (rate limiting, quota, outage, overload, timeout)\n * are failover-eligible: horizontal recovery within the tier is exactly\n * their remedy.\n * - Permanent configuration failures (auth, invalid model, content policy)\n * and context overflow never retry and never failover: retrying the same\n * request cannot help and switching resources would reproduce the failure.\n * - User cancellation is final: no retry, no failover (ADP-010).\n * - Unknown failures defer to OpenCode's native handling.\n */\nconst CATEGORY_POLICY: Readonly<\n Record<FailureCategory, { retryable: boolean; failoverEligible: boolean }>\n> = {\n rate_limited: { retryable: true, failoverEligible: true },\n quota_exhausted: { retryable: false, failoverEligible: true },\n outage: { retryable: true, failoverEligible: true },\n overload: { retryable: true, failoverEligible: true },\n timeout: { retryable: true, failoverEligible: true },\n auth_failure: { retryable: false, failoverEligible: false },\n invalid_model: { retryable: false, failoverEligible: false },\n context_overflow: { retryable: false, failoverEligible: false },\n content_policy: { retryable: false, failoverEligible: false },\n user_cancelled: { retryable: false, failoverEligible: false },\n unknown: { retryable: false, failoverEligible: false },\n}\n\n/**\n * Error tags from the documented @opencode/protocol vocabulary that map\n * directly to a failure category. Tags absent here (`ProviderRequestError`,\n * `UnknownError`, anything unrecognized) are structured but not\n * category-specific: they never guess, and classification falls through to\n * the refining layers.\n */\nconst STRUCTURED_TYPES = new Map<string, FailureCategory>([\n [\"UnauthorizedError\", \"auth_failure\"],\n [\"ForbiddenError\", \"auth_failure\"],\n [\"ModelNotFoundError\", \"invalid_model\"],\n [\"UnsupportedModelError\", \"invalid_model\"],\n [\"ServiceUnavailableError\", \"outage\"],\n])\n\n/**\n * Bounded textual patterns, last resort only (ADP-008). Ordered most\n * specific first; the first match wins. These encode provider error-message\n * conventions documented by the research phase, not a general-purpose\n * keyword classifier.\n */\nconst TEXTUAL_PATTERNS: readonly {\n readonly category: FailureCategory\n readonly pattern: RegExp\n}[] = [\n {\n category: \"context_overflow\",\n pattern:\n /context (length|window)|maximum context|input length exceeds|prompt is too long|too many (input )?tokens|exceeds the maximum/i,\n },\n {\n // Subscription limits (\"You've hit your session/usage limit\") are quota\n // exhaustion with a provider-side reset window; the reset time in the\n // text is not parsed (the classifier has no clock) — the health layer's\n // default quota window bounds the quarantine instead.\n // Sibling fallback plugins' issue trackers document the real-world\n // phrasings that missed curated whitelists (oh-my-openagent #2393/#5815,\n // lichter's 429 lesson): snake-cased limits, \"usage exceeded\" without\n // \"limit\", credit exhaustion without \"balance\". Qualifiers stay required\n // so \"token limit reached\" (context overflow) never matches quota.\n category: \"quota_exhausted\",\n pattern:\n /\\bquota\\b|credit balance|(?:session|usage|free usage|message|plan|monthly|weekly|daily)[ _-]?limit|usage exceeded|out of credits|insufficient credits/i,\n },\n { category: \"rate_limited\", pattern: /rate ?limit/i },\n { category: \"timeout\", pattern: /\\btimeout\\b|timed? ?out|deadline exceeded|etimedout/i },\n {\n category: \"auth_failure\",\n pattern:\n /unauthorized|invalid (api|access) key|api key (expired|invalid)|authentication|permission denied/i,\n },\n {\n category: \"invalid_model\",\n pattern: /model.{0,30}(does not exist|not found)|invalid model|unknown model|no such model/i,\n },\n {\n category: \"content_policy\",\n pattern:\n /content (management )?(policy|filter)|safety system|violates?.{0,32}policy|flagged as/i,\n },\n { category: \"overload\", pattern: /overloaded|over capacity|high demand/i },\n { category: \"user_cancelled\", pattern: /cancel+(l)?ed|aborted by (the )?user|user abort/i },\n]\n\nfunction isHttpStatus(status: number | undefined): status is number {\n return typeof status === \"number\" && Number.isInteger(status) && status >= 100 && status <= 599\n}\n\n/**\n * Upper bound on a provider-claimed retry delay. `Retry-After` and structured\n * retry metadata are untrusted provider data: a hostile or misconfigured\n * value (\"999999999\" seconds) must not convert one response into a\n * near-permanent resource blacklist. The host's own retry scheduling waits\n * out the raw header verbatim — this bound only concerns the health window\n * the supervisor derives from the classification (ADP-011 boundedness).\n */\nconst MAX_RETRY_AFTER_MS = 24 * 60 * 60 * 1000\n\nfunction boundedRetryAfterMs(retryAfterMs: number | undefined): number | undefined {\n if (retryAfterMs === undefined) {\n return undefined\n }\n return Math.min(retryAfterMs, MAX_RETRY_AFTER_MS)\n}\n\nfunction isNonNegativeInteger(value: number | undefined): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0\n}\n\nfunction lowercasedHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n): ReadonlyMap<string, string> {\n const lowered = new Map<string, string>()\n if (headers === undefined) {\n return lowered\n }\n for (const [name, value] of Object.entries(headers)) {\n lowered.set(name.toLowerCase(), value)\n }\n return lowered\n}\n\n/**\n * Parses a `Retry-After` header into milliseconds. Only the integer-seconds\n * form is accepted: the HTTP-date form requires a clock, which the classifier\n * deliberately does not have (reset-time handling belongs to the capacity\n * boundary, T-05/T-15).\n */\nfunction retryAfterHeaderMs(headers: ReadonlyMap<string, string>): number | undefined {\n const raw = headers.get(\"retry-after\")\n if (raw === undefined || !/^\\d+$/.test(raw)) {\n return undefined\n }\n return Number(raw) * 1000\n}\n\n/**\n * Standard rate-limit headers (case-insensitive `x-ratelimit-remaining*`).\n * All-zero remaining capacity indicates a fixed-window quota exhaustion\n * rather than a short burst limit.\n */\nfunction fixedWindowExhausted(headers: ReadonlyMap<string, string>): boolean {\n let sawRemaining = false\n for (const [name, value] of headers) {\n if (name.startsWith(\"x-ratelimit-remaining\")) {\n sawRemaining = true\n if (value !== \"0\") {\n return false\n }\n }\n }\n return sawRemaining\n}\n\nfunction retryAdvice(category: FailureCategory, retryAfterMs: number | undefined): RetryAdvice {\n const policy = CATEGORY_POLICY[category]\n const bounded = boundedRetryAfterMs(retryAfterMs)\n if (bounded === undefined) {\n return { retryable: policy.retryable }\n }\n return { retryable: policy.retryable, retryAfterMs: bounded }\n}\n\nfunction classification(\n category: FailureCategory,\n layer: FailureClassification[\"layer\"],\n definitive: boolean,\n retryAfterMs: number | undefined,\n quotaScope?: \"provider\",\n): FailureClassification {\n const policy = CATEGORY_POLICY[category]\n return {\n category,\n layer,\n definitive,\n retry: retryAdvice(category, retryAfterMs),\n failoverEligible: policy.failoverEligible,\n ...(quotaScope !== undefined ? { quotaScope } : {}),\n }\n}\n\n/**\n * Classifies an HTTP status. 429 consults the standard rate-limit headers in\n * the same step: status plus documented standard headers are one HTTP\n * semantics layer, and the quota refinement is more specific evidence than a\n * bare 429, so it cannot weaken the ADP-008 precedence.\n */\nfunction classifyStatus(\n status: number,\n headers: ReadonlyMap<string, string>,\n): FailureCategory | undefined {\n if (status === 429) {\n return fixedWindowExhausted(headers) ? \"quota_exhausted\" : \"rate_limited\"\n }\n if (status === 401 || status === 403) {\n return \"auth_failure\"\n }\n if (status === 408) {\n return \"timeout\"\n }\n if (status === 413) {\n return \"context_overflow\"\n }\n if (status === 502 || status === 503 || status === 504) {\n return \"outage\"\n }\n return undefined\n}\n\nfunction classifyText(text: string): FailureCategory | undefined {\n const bounded = text.length > TEXT_BOUND ? text.slice(0, TEXT_BOUND) : text\n for (const { category, pattern } of TEXTUAL_PATTERNS) {\n if (pattern.test(bounded)) {\n return category\n }\n }\n return undefined\n}\n\n/**\n * Total classification of layered failure evidence. Never throws and always\n * returns a category: any evidence shape outside the documented vocabularies\n * classifies as `unknown` via the `none` layer, deferring to OpenCode's\n * native failure handling (ADP-009, ADP-012).\n */\nexport function classifyFailure(evidence: ErrorEvidence): FailureClassification {\n // Layer 0: interruption evidence. Interruptions are host decisions, not\n // provider failures; a user cancellation is final (ADP-010).\n if (evidence.interrupted === \"user\") {\n return classification(\"user_cancelled\", \"interruption\", true, undefined)\n }\n if (evidence.interrupted !== undefined) {\n return classification(\"unknown\", \"interruption\", true, undefined)\n }\n\n // Layer 1: structured OpenCode metadata.\n const structured = evidence.structured\n const structuredRetryAfterMs = isNonNegativeInteger(structured?.retryAfterMs)\n ? structured.retryAfterMs\n : undefined\n if (structured !== undefined) {\n const mapped = STRUCTURED_TYPES.get(structured.type)\n if (mapped !== undefined) {\n return classification(mapped, \"structured\", true, structuredRetryAfterMs)\n }\n // Unmapped tag: fall through so HTTP semantics can refine the category.\n }\n\n // Layer 2: HTTP status (+ standard headers for the 429 refinement).\n const headers = lowercasedHeaders(evidence.headers)\n if (isHttpStatus(evidence.status)) {\n const category = classifyStatus(evidence.status, headers)\n if (category !== undefined) {\n const retryAfterMs = structuredRetryAfterMs ?? retryAfterHeaderMs(headers)\n if (category === \"rate_limited\" && evidence.text !== undefined) {\n const bounded = evidence.text.slice(0, TEXT_BOUND)\n if (classifyText(bounded) === \"quota_exhausted\" && !RATE_LIMIT_TEXT.test(bounded)) {\n const quotaScope = SHARED_QUOTA_PATTERN.test(bounded) ? \"provider\" : undefined\n return classification(\"quota_exhausted\", \"status\", true, retryAfterMs, quotaScope)\n }\n }\n return classification(category, \"status\", true, retryAfterMs)\n }\n }\n\n // Layer 3: standard headers alone (Retry-After without a decisive status).\n const headerRetryAfterMs = retryAfterHeaderMs(headers)\n if (headerRetryAfterMs !== undefined) {\n return classification(\"rate_limited\", \"headers\", false, headerRetryAfterMs)\n }\n\n // Layer 4: documented provider-specific metadata, normalized by our own\n // adapters after validation (T-15).\n if (evidence.providerMetadata?.quotaExhausted === true) {\n return classification(\"quota_exhausted\", \"provider-metadata\", true, undefined)\n }\n if (evidence.providerMetadata?.overloaded === true) {\n return classification(\"overload\", \"provider-metadata\", true, structuredRetryAfterMs)\n }\n\n // Layer 5: bounded textual classification, always non-definitive.\n const text = evidence.text\n if (text !== undefined) {\n const category = classifyText(text)\n if (category !== undefined) {\n const quotaScope =\n category === \"quota_exhausted\" && SHARED_QUOTA_PATTERN.test(text.slice(0, TEXT_BOUND))\n ? \"provider\"\n : undefined\n return classification(category, \"textual\", false, structuredRetryAfterMs, quotaScope)\n }\n }\n\n return classification(\"unknown\", \"none\", false, undefined)\n}\n"]}
@@ -909,7 +909,7 @@ export class Supervisor {
909
909
  status: report.error.status,
910
910
  // Terminal classification may use the bounded textual last resort
911
911
  // (content-policy rejections arrive as free text, not error types).
912
- // The retry hook deliberately omits text: it must stay conservative.
912
+ // The retry hook passes text only to refine a 429's window.
913
913
  text: report.error.message,
914
914
  });
915
915
  const countTerminal = this.#countTerminalFailure(unitID, classification.category);
@@ -1254,6 +1254,9 @@ export class Supervisor {
1254
1254
  : {}),
1255
1255
  },
1256
1256
  status: input.error.status,
1257
+ // Text only refines a definitive 429's window (rate limit vs spent
1258
+ // quota); every other retry classification stays structural.
1259
+ ...(input.error.status === 429 ? { text: input.error.message } : {}),
1257
1260
  });
1258
1261
  const unitID = this.#tracker.active(input.sessionID)?.id;
1259
1262
  // The pumped http-response event describes the same failed request;