@coool/opencode-adaptive 1.4.0 → 1.5.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.
- package/README.md +2 -1
- package/dist/adapters/opencode/index.d.ts +2 -0
- package/dist/adapters/opencode/index.js +2 -0
- package/dist/adapters/opencode/index.js.map +1 -1
- package/dist/adapters/opencode/process-ledger.d.ts +56 -0
- package/dist/adapters/opencode/process-ledger.js +143 -0
- package/dist/adapters/opencode/process-ledger.js.map +1 -0
- package/dist/adapters/opencode/session-ops.d.ts +2 -0
- package/dist/adapters/opencode/session-ops.js +15 -9
- package/dist/adapters/opencode/session-ops.js.map +1 -1
- package/dist/adapters/opencode/timer.d.ts +5 -0
- package/dist/adapters/opencode/timer.js +12 -0
- package/dist/adapters/opencode/timer.js.map +1 -0
- package/dist/execution/parking/index.d.ts +2 -1
- package/dist/execution/parking/index.js +2 -1
- package/dist/execution/parking/index.js.map +1 -1
- package/dist/execution/parking/wake.d.ts +52 -0
- package/dist/execution/parking/wake.js +95 -0
- package/dist/execution/parking/wake.js.map +1 -0
- package/dist/gui-experimental.cjs +3 -3
- package/dist/gui.d.ts +2 -2
- package/dist/gui.js +37 -3
- package/dist/gui.js.map +1 -1
- package/dist/health/classifier/classify.js +9 -0
- package/dist/health/classifier/classify.js.map +1 -1
- package/dist/index.js +33 -20
- package/dist/index.js.map +1 -1
- package/dist/rpc/status.d.ts +9 -0
- package/dist/rpc/status.js +1 -0
- package/dist/rpc/status.js.map +1 -1
- package/dist/state/parked-work/store.d.ts +5 -0
- package/dist/state/parked-work/store.js +24 -0
- package/dist/state/parked-work/store.js.map +1 -1
- package/dist/supervisor/supervisor.d.ts +2 -0
- package/dist/supervisor/supervisor.js +475 -106
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/supervisor/types.d.ts +5 -0
- package/dist/supervisor/types.js.map +1 -1
- package/dist/tui.d.ts +13 -0
- package/dist/tui.js +62 -16
- package/dist/tui.js.map +1 -1
- package/dist/ui/flow/waiting.d.ts +11 -0
- package/dist/ui/flow/waiting.js +40 -0
- package/dist/ui/flow/waiting.js.map +1 -0
- package/dist/ui/gui/api.d.ts +2 -0
- package/dist/ui/gui/api.js +6 -2
- package/dist/ui/gui/api.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const
|
|
2
|
-
`)}function
|
|
3
|
-
`)),t}})}function
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const c=require("solid-js/web"),T=require("@opencode/plugin/gui"),f=require("solid-js"),Z=require("@opencode/schema/rpc"),Q=1e4,ee=1440*6e4,te=6;function ie(e){const t={};for(const i of e.waiting)t[i.sessionID]=i.resumeAt;return t}function j(e){const t=new Date(e);return new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime()}function C(e){return`${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`}function ne(e,t){if(e===null)return"waiting";if(e<=t)return"waiting · resuming";const i=new Date(e),n=Math.round((j(e)-j(t))/ee);if(n===0)return`waiting · resumes ${C(i)}`;const o=i.toDateString();return n<=te?`waiting · resumes ${o.slice(0,3)} ${C(i)}`:`waiting · resumes ${o.slice(4,7)} ${String(i.getDate())} ${C(i)}`}const oe=128,re="coool.adaptive",x="coool.adaptive",S={internal:_()};function p(e,t){return{"~standard":{version:1,vendor:x,validate:i=>e(i)?{value:i}:{issues:[{message:t}]}}}}function _(){return{"~standard":{version:1,vendor:x,validate:e=>e===void 0?{value:void 0}:{issues:[{message:"unexpected input: no input expected"}]}}}}const g=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),k=e=>typeof e=="string",M=e=>{if(!(g(e)&&k(e.sessionID)))return!1;const t=e.sessionID;return t.length>0&&t.length<=oe},se=e=>g(e)&&k(e.sessionID)&&(e.mode===null||k(e.mode)),ae=e=>g(e)&&k(e.sessionID)&&(e.tier===null||k(e.tier)),V=Z.Rpc.define({id:re,methods:{"settings.get":{errors:S,input:_(),output:p(g,"output must be an object")},"settings.set":{errors:S,input:p(g,"input must be an object"),output:p(g,"output must be an object")},"status.get":{errors:S,input:_(),output:p(g,"output must be an object")},"receipts.list":{errors:S,input:_(),output:p(Array.isArray,"output must be an array")},"session-mode.get":{errors:S,input:p(M,"input must be an object with a sessionID string"),output:p(g,"output must be an object")},"session-mode.set":{errors:S,input:p(se,"input must be an object with a sessionID string and a mode string or null"),output:p(g,"output must be an object")},"tier-request.get":{errors:S,input:p(M,"input must be an object with a sessionID string"),output:p(g,"output must be an object")},"tier-request.set":{errors:S,input:p(ae,"input must be an object with a sessionID string and a tier string or null"),output:p(g,"output must be an object")}},events:{"settings-changed":{schema:p(g,"event data must be an object")},"session-mode-changed":{schema:p(M,"event data must be an object with a sessionID string")},"tier-request-changed":{schema:p(M,"event data must be an object with a sessionID string")}}}),R=["off","observe","active"],w=["quick","standard","advanced"];function ce(e,t){const i=e.rpc(V),n=()=>{};return{status:()=>i["status.get"](void 0,t()).catch(n),waiting:()=>i["status.get"](void 0,t()).then(ie).catch(n),receipts:()=>i["receipts.list"](void 0,t()).catch(n),update:async o=>{try{return(await i["settings.set"](o,t())).ok}catch{return!1}},watchSettings:o=>i.events.on("settings-changed",()=>{o()})}}function h(e){const t=`${e.providerID}/${e.id}`;return e.variant===void 0?t:`${t}@${e.variant}`}function F(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 ue(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:`${h(r)}${F(o==null?void 0:o[s])}`,value:{kind:"resource",index:s,ref:r},description:"Move or remove"})}),n}function de(e){return w.map(t=>{const i=e.tiers[t],n=i.length===0?"(empty — add models to enable routing)":i.map(o=>h(o.ref)).join(" · ");return{title:`${t} (${String(i.length)})`,value:t,description:n}})}function le(e){return e.map(t=>({title:`${t.providerID}/${t.modelID}`,description:t.name,value:t}))}function pe(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 ve(e,t){return[...e.filter((i,n)=>n===t),...e.filter((i,n)=>n!==t)]}function fe(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 ge(e,t){return e.filter((i,n)=>n!==t)}function he(e){const t=[];for(const i of w)for(const n of e.tiers[i])t.push({tier:i,status:n});return t}function q(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 me(e,t){return t===null?!1:t.providerID===e.providerID&&t.id===e.id&&t.variant===e.variant}function we(e,t){let i="";return e.windowed&&(i+=e.windowUntil===null?", windowed (deadline unknown)":`, windowed until ${q(e.windowUntil)}`),e.usedPercent!==null&&(i+=`, used ${String(e.usedPercent)}%`,e.resetAt!==null&&(i+=`, resets ${q(e.resetAt)}`)),e.breaker==="open"?i+=", breaker open":e.breaker==="half_open"&&(i+=", probing"),me(e.ref,t)&&(i+=" (pinned)"),i}function be(e,t){return we(e,t).replace(/^[, ]+/,"")||"-"}function $e(e){const t=["MODEL","TIER","HEALTH","STATE"],i=he(e).map(({tier:r,status:s})=>[h(s.ref),r,s.health,be(s,e.pin)]),n=t.map((r,s)=>Math.max(r.length,...i.map(a=>a[s].length))),o=r=>r.map((s,a)=>s.padEnd(n[a])).join(" ").trimEnd();return[o(t),...i.map(o)]}const Se="No models configured yet — open Models to add some.",ye=["status","models","mode","tier","pin","receipts"];function De(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 Ie(e){return w.map(i=>`${i}: ${String(e.tiers[i].length)}`).join(" · ")}function ke(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 Ae(e,t){if(e===t.override)return"override";if(e===t.effective)return"current"}function Te(e,t,i,n=e.mode){const o=r=>{switch(r){case"status":return{title:"Status",value:r,description:De(t)};case"models":return{title:"Models",value:r,description:Ie(e)};case"mode":return{title:"Mode",value:r,description:`currently ${n}`};case"tier":return{title:"Tier",value:r,description:ke(i)};case"pin":return{title:"Pin",value:r,description:e.pin===null?"force one resource":`pinned to ${h(e.pin)}`};case"receipts":return{title:"Receipts",value:r,description:"why each turn ran the model it ran"}}};return ye.map(o)}function Me(e){return R.map(t=>({title:t===e?`${t} (current)`:t,value:t,description:G(t)}))}function G(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 _e(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 Ee(e){const t=[{title:`Follow global (${e.global})`,description:"clear this session's override",value:{kind:"global"}}];for(const i of R){const n=Ae(i,e);t.push({title:n===void 0?i:`${i} (${n})`,value:{kind:"mode",mode:i},description:G(i)})}return t}function Re(e){switch(e){case"quick":return"routine edits, mechanical steps";case"standard":return"the default";case"advanced":return"multi-step design, broad refactors"}}function Oe(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:Re(i)});return t}function W(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 P(e){switch(e.kind){case"apply":{const t=e.tier===void 0?"":` [${e.tier}]`;return`apply ${h(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} → ${h(e.target)} [${e.tier}]`;case"observe-only":return`observe: ${P(e.would)}`}}function H(e){return e===void 0?"pending":e.status==="succeeded"?"succeeded":e.status==="failed"?`failed (${e.category})`:`interrupted (${e.reason})`}function U(e){return`#${String(e.id)} ${P(e.decision)} — ${H(e.outcome)}`}function Ce(e,t=Date.now()){return[`id: ${String(e.id)}`,`when: ${Pe(e.at,t)} (${new Date(e.at).toISOString()})`,`session: ${e.sessionID}`,`unit: ${e.unitID??"-"}`,`tier: ${e.tier??"-"}`,`decision: ${P(e.decision)}`,`outcome: ${H(e.outcome)}`,"",`What happened: ${je(e)}`].join(`
|
|
2
|
+
`)}function Pe(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 je(e){const t=e.decision;switch(t.kind){case"apply":return qe[t.reason];case"defer":return Ne[t.reason];case"park":return Le[t.reason];case"escalate":return`This turn needed stronger capability, so Adaptive moved it to ${h(t.target)}.`;case"observe-only":return"Adaptive is observing: this is what it would have done, without applying it."}}const qe={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."},Ne={"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)."},Le={"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."},E=20;function xe(e){return e.slice(0,E)}const N="OpenCode Adaptive",Ve="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(W(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 L(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 Fe(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 Ge(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:N,message:Ve});const s=await e.view.select({title:N,options:[{title:"Open the menu now",value:!0},{title:"Later",value:!1}]}),a=await y(e,{inviteDismissed:!0},n);if(a!==void 0&&(i=a),s!==!0)return}for(;;){const[s,a]=await Promise.all([e.api.settings().catch(()=>{}),e.api.status().catch(()=>{})]);s!==void 0&&(i=s);const l=o===void 0?void 0:await e.api.tierRequest(o).catch(()=>{}),d=await e.view.select({title:`Adaptive (${r??i.mode})`,options:Te(i,a,l,r??i.mode)});if(d===void 0)return;if(d==="status"){await Ue(e);continue}if(d==="models"){i=await Ye(e,n)??i;continue}if(d==="mode"){const v=await We(e,i,n,o,r);v!==void 0&&(i=v.settings,r=v.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 He(e,o,n);continue}if(d==="pin"){i=await Xe(e,i,n)??i;continue}await Ze(e)}}async function We(e,t,i,n,o){for(;;){const r=await e.view.select({title:"Operating mode",options:_e(t.mode,o??t.mode)});if(r===void 0)return;if(r==="global"){const l=await e.view.select({title:"Global mode",options:Me(t.mode)});if(l===void 0||l===t.mode)return;const d=await y(e,{mode:l},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:Ee({global:t.mode,override:o,effective:o??t.mode})});return s===void 0?void 0:s.kind==="global"?o===null||await L(e,n,null,i)===void 0?void 0:{settings:t,override:null}:s.mode===(o??t.mode)||await L(e,n,s.mode,i)===void 0?void 0:{settings:t,override:s.mode}}}async function He(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:Oe({pinned:n})});if(o===void 0)return;const r=o.kind==="ladder"?null:o.tier;r!==n&&await Fe(e,t,r,i)}async function Ue(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:Se});return}await e.view.table({title:"Status",lines:$e(t)})}async function Ye(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:de(o)});if(r===void 0)return i;let s=o,a=n;for(;;){const l=await e.view.select({title:`Tier: ${r}`,options:ue(a,r,s)});if(l===void 0)break;if(l.kind==="add"){const u=await ze(e,a,r,t);u!==void 0&&(i=u,a=u)}else{const u=await Be(e,a,r,l,t);u!==void 0&&(i=u,a=u)}const[d,v]=await Promise.all([e.api.settings().catch(()=>{}),e.api.status().catch(()=>{})]);d!==void 0&&(i=d,a=d),v!==void 0&&(s=v)}}}async function ze(e,t,i,n){const o=e.models(),r=await e.view.select({title:"Add model",placeholder:"Search provider / model / name",options:le(o)});if(r===void 0)return;const s=r.variants;let a;if(s.length>0){const v=await e.view.select({title:`Variant: ${r.providerID}/${r.modelID}`,options:pe(s)});if(v===void 0)return;a=v.variant}const l=a===void 0?{providerID:r.providerID,id:r.modelID}:{providerID:r.providerID,id:r.modelID,variant:a},d=[...t.tiers[i],l];return y(e,{tiers:{...t.tiers,[i]:d}},n)}function Je(){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 Be(e,t,i,{index:n,ref:o},r){const s=await e.view.select({title:`Resource: ${h(o)}`,options:Je()});if(s===void 0)return;if(s==="remove")return await e.view.confirm({title:"Remove resource",message:`Remove ${h(o)} from ${i}?`})!==!0?void 0:y(e,{tiers:{...t.tiers,[i]:ge(t.tiers[i],n)}},r);const a=s==="top"?ve(t.tiers[i],n):fe(t.tiers[i],n,s==="up"?-1:1);return y(e,{tiers:{...t.tiers,[i]:a}},r)}function Ke(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:`${h(n)} (${i})`,description:"route every turn to this resource, even in active mode",value:{kind:"pin",ref:n}});return t}async function Xe(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:Ke(t)});if(o!==void 0)return y(e,{pin:o.kind==="clear"?null:o.ref},i)}async function Ze(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=xe(t.slice(i)),o=i+E<t.length,r=await e.view.selectPaged({title:`Receipts (${String(t.length)}, newest first)`,options:n.map(a=>({title:U(a),value:a})),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+=E;continue}if(r.action==="prev"){i=Math.max(0,i-E);continue}const s=r.picked;if(s===void 0)return;await e.view.alert({title:`Receipt #${String(s.id)}`,message:Ce(s)})}}}var Qe=c.template("<pre class=adaptive-table>");function et(e){return f.onCleanup(e.done),c.createComponent(e.host.ui.Dialog,{get title(){return e.title},size:"x-large",get footer(){return c.createComponent(e.host.ui.Button,{get onClick(){return e.close},children:"Close"})},get children(){var t=Qe();return c.insert(t,()=>e.lines.join(`
|
|
3
|
+
`)),t}})}function tt(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=>c.createComponent(et,{host:e,title:t,lines:i,close:()=>{o.close()},done:n}))}),toast:(t,i)=>{e.toast.show({title:t,variant:i})}}}function Y(e){const t=e.sessions.current();return(t==null?void 0:t.server.builtin)===!0?t:void 0}function z(e){var i,n;const t=(n=(i=Y(e))==null?void 0:i.location)==null?void 0:n.directory;return t===void 0?{}:{location:{directory:t}}}function it(e){const t=e.client.rpc(V),i=()=>z(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 nt(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 ot(e){const t=tt(e),i=Y(e),n=nt(e);await(async()=>{await(n==null?void 0:n.sync(i==null?void 0:i.location).catch(()=>{})),await Ge({api:it(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(W(r),"error")})}const rt="connection";function st(e){if(e===void 0)return[{id:rt,label:"Server",value:"disconnected",tone:"warning"}];const t=[{id:"mode",label:"Mode",value:e.mode},{id:"pin",label:"Pin",value:e.pin===null?"none":h(e.pin)}];for(const i of w){const n=e.tiers[i],o=n.length===0?"empty":n.map(r=>`${h(r.ref)}${F(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 at(e){return R.map(t=>({value:t,title:t===e?`${t} (current)`:t}))}function ct(e){return R.find(t=>t===e)}function ut(e){return e.map(t=>U(t))}var dt=c.template("<span class=adaptive-saved role=status>Saved"),lt=c.template("<div class=adaptive-receipts>"),pt=c.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>'),vt=c.template("<tr class=adaptive-row><td class=adaptive-row-label></td><td class=adaptive-row-value>"),ft=c.template("<option>"),gt=c.template("<div class=adaptive-receipt-line>");const ht=5;function J(e){const[t,i]=f.createSignal(void 0),[n,o]=f.createSignal("observe"),[r,s]=f.createSignal([]),[a,l]=f.createSignal(!1),d=()=>{e.api.status().then(u=>{i(u),u!==void 0&&o(u.mode)}),e.api.receipts().then(u=>{s(u??[])})};f.onCleanup(e.api.watchSettings(d)),d();const v=u=>{const D=ct(u.value);u.value=n(),D!==void 0&&e.api.update({mode:D}).then(O=>{l(O),d()})};return(()=>{var u=pt(),D=u.firstChild,O=D.firstChild,B=D.nextSibling,K=B.firstChild,A=K.nextSibling;return c.insert(O,c.createComponent(f.For,{get each(){return st(t())},children:m=>(()=>{var $=vt(),I=$.firstChild,X=I.nextSibling;return c.insert(I,()=>m.label),c.insert(X,()=>m.value),c.effect(()=>c.setAttribute($,"data-tone",m.tone)),$})()})),A.addEventListener("change",m=>{v(m.currentTarget)}),c.insert(A,c.createComponent(f.For,{get each(){return at(n())},children:m=>(()=>{var $=ft();return c.insert($,()=>m.title),c.effect(()=>$.value=m.value),$})()})),c.insert(u,c.createComponent(f.Show,{get when(){return a()},get children(){return dt()}}),null),c.insert(u,c.createComponent(f.Show,{get when(){return t()!==void 0},get children(){var m=lt();return c.insert(m,c.createComponent(f.For,{get each(){return ut(r()).slice(0,ht)},children:$=>(()=>{var I=gt();return c.insert(I,$),I})()})),m}}),null),c.effect(()=>A.disabled=t()===void 0),c.effect(()=>A.value=n()),u})()}const mt=T.Plugin.define({setup(e){const t=ce(e.client,()=>z(e));e.add(T.SettingsPage,{id:"adaptive",title:"OpenCode Adaptive",entries:[{id:"adaptive",title:"OpenCode Adaptive",description:"Execution tier routing, operating mode and resource health"}],render:()=>c.createComponent(J,{api:t})}),e.add(T.Command,{id:"open",title:"OpenCode Adaptive",group:"Adaptive",suggested:!0,slash:{name:"adaptive"},run:()=>ot(e)});const[i,n]=f.createSignal({sessions:{},now:0}),o=()=>{t.waiting().then(a=>{a!==void 0&&n({sessions:a,now:Date.now()})})};o();const r=setInterval(o,Q),s=t.watchSettings(o);return e.add(T.ComposerPill,{id:"waiting",show:a=>{const{sessions:l,now:d}=i(),v=l[a.session.id];if(v===void 0)return;const u=ne(v,d);return{label:u.charAt(0).toUpperCase()+u.slice(1),icon:"status",tone:"warning",title:"Waiting for quota: Adaptive continues this session automatically"}}}),()=>{clearInterval(r),s()}}});exports.AdaptiveStatusPage=J;exports.default=mt;
|
package/dist/gui.d.ts
CHANGED
|
@@ -10,8 +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
|
|
14
|
-
* routing logic lives here (ADP-034/035).
|
|
13
|
+
* SettingsPage, a palette/`/adaptive` slash command that runs the same dialog flow as the TUI (ui/flow), and a
|
|
14
|
+
* composer pill on sessions parked until quota returns; no routing logic lives here (ADP-024/034/035).
|
|
15
15
|
*/
|
|
16
16
|
declare const _default: Plugin.Definition<{}>;
|
|
17
17
|
export default _default;
|
package/dist/gui.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "solid-js/jsx-runtime";
|
|
2
|
-
import { Command, Plugin, SettingsPage } from "@opencode/plugin/gui";
|
|
2
|
+
import { Command, ComposerPill, 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
5
|
import { openAdaptive, rpcLocation } from "./ui/gui/flow.js";
|
|
6
6
|
import { modeChoices, parseModeValue, receiptLines, statusRows } from "./ui/gui/view.js";
|
|
7
|
+
import { WAITING_POLL_MS, waitingLabel } from "./ui/flow/waiting.js";
|
|
7
8
|
const RECEIPTS_PREVIEW_LIMIT = 5;
|
|
8
9
|
/**
|
|
9
10
|
* Native GUI settings page body (ADP-034). All data comes from the typed RPC adapter; the component holds no
|
|
@@ -44,8 +45,8 @@ export function AdaptiveStatusPage(props) {
|
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* GUI plugin entry (the `./gui-experimental` bundle default export). Thin adapter: registers a native
|
|
47
|
-
* SettingsPage
|
|
48
|
-
* routing logic lives here (ADP-034/035).
|
|
48
|
+
* SettingsPage, a palette/`/adaptive` slash command that runs the same dialog flow as the TUI (ui/flow), and a
|
|
49
|
+
* composer pill on sessions parked until quota returns; no routing logic lives here (ADP-024/034/035).
|
|
49
50
|
*/
|
|
50
51
|
export default Plugin.define({
|
|
51
52
|
setup(context) {
|
|
@@ -70,6 +71,39 @@ export default Plugin.define({
|
|
|
70
71
|
slash: { name: "adaptive" },
|
|
71
72
|
run: () => openAdaptive(context),
|
|
72
73
|
});
|
|
74
|
+
// Work parks and resumes on the server without any GUI action (a failed execution, the wake timer), so the
|
|
75
|
+
// pill polls; a failed read keeps the last known state.
|
|
76
|
+
const [waiting, setWaiting] = createSignal({ sessions: {}, now: 0 });
|
|
77
|
+
const refreshWaiting = () => {
|
|
78
|
+
void api.waiting().then((sessions) => {
|
|
79
|
+
if (sessions !== undefined)
|
|
80
|
+
setWaiting({ sessions, now: Date.now() });
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
refreshWaiting();
|
|
84
|
+
const poll = setInterval(refreshWaiting, WAITING_POLL_MS);
|
|
85
|
+
// Only active sessions wait: a mode change can start or end a wait.
|
|
86
|
+
const unwatch = api.watchSettings(refreshWaiting);
|
|
87
|
+
context.add(ComposerPill, {
|
|
88
|
+
id: "waiting",
|
|
89
|
+
show: (input) => {
|
|
90
|
+
const { sessions, now } = waiting();
|
|
91
|
+
const resumeAt = sessions[input.session.id];
|
|
92
|
+
if (resumeAt === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const label = waitingLabel(resumeAt, now);
|
|
95
|
+
return {
|
|
96
|
+
label: label.charAt(0).toUpperCase() + label.slice(1),
|
|
97
|
+
icon: "status",
|
|
98
|
+
tone: "warning",
|
|
99
|
+
title: "Waiting for quota: Adaptive continues this session automatically",
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return () => {
|
|
104
|
+
clearInterval(poll);
|
|
105
|
+
unwatch();
|
|
106
|
+
};
|
|
73
107
|
},
|
|
74
108
|
});
|
|
75
109
|
//# sourceMappingURL=gui.js.map
|
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 { 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"]}
|
|
1
|
+
{"version":3,"file":"gui.js","sourceRoot":"","sources":["../src/gui.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Command, ComposerPill, 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\"\nimport { WAITING_POLL_MS, waitingLabel } from \"./ui/flow/waiting.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, a palette/`/adaptive` slash command that runs the same dialog flow as the TUI (ui/flow), and a\n * composer pill on sessions parked until quota returns; no routing logic lives here (ADP-024/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 // Work parks and resumes on the server without any GUI action (a failed execution, the wake timer), so the\n // pill polls; a failed read keeps the last known state.\n const [waiting, setWaiting] = createSignal<{\n sessions: Record<string, number | null>\n now: number\n }>({ sessions: {}, now: 0 })\n const refreshWaiting = (): void => {\n void api.waiting().then((sessions) => {\n if (sessions !== undefined) setWaiting({ sessions, now: Date.now() })\n })\n }\n refreshWaiting()\n const poll = setInterval(refreshWaiting, WAITING_POLL_MS)\n // Only active sessions wait: a mode change can start or end a wait.\n const unwatch = api.watchSettings(refreshWaiting)\n context.add(ComposerPill, {\n id: \"waiting\",\n show: (input) => {\n const { sessions, now } = waiting()\n const resumeAt = sessions[input.session.id]\n if (resumeAt === undefined) return undefined\n const label = waitingLabel(resumeAt, now)\n return {\n label: label.charAt(0).toUpperCase() + label.slice(1),\n icon: \"status\",\n tone: \"warning\",\n title: \"Waiting for quota: Adaptive continues this session automatically\",\n }\n },\n })\n return () => {\n clearInterval(poll)\n unwatch()\n }\n },\n})\n"]}
|
|
@@ -188,6 +188,9 @@ function classifyStatus(status, headers) {
|
|
|
188
188
|
if (status === 401 || status === 403) {
|
|
189
189
|
return "auth_failure";
|
|
190
190
|
}
|
|
191
|
+
if (status === 402) {
|
|
192
|
+
return "quota_exhausted";
|
|
193
|
+
}
|
|
191
194
|
if (status === 408) {
|
|
192
195
|
return "timeout";
|
|
193
196
|
}
|
|
@@ -241,6 +244,12 @@ export function classifyFailure(evidence) {
|
|
|
241
244
|
const category = classifyStatus(evidence.status, headers);
|
|
242
245
|
if (category !== undefined) {
|
|
243
246
|
const retryAfterMs = structuredRetryAfterMs ?? retryAfterHeaderMs(headers);
|
|
247
|
+
// Payment Required is the account's balance, not one model's limit:
|
|
248
|
+
// every model behind the provider ID (a gateway such as OpenRouter or
|
|
249
|
+
// Ofox) bills the same account, so a sibling model cannot help.
|
|
250
|
+
if (evidence.status === 402) {
|
|
251
|
+
return classification(category, "status", true, retryAfterMs, "provider");
|
|
252
|
+
}
|
|
244
253
|
if (category === "rate_limited" && evidence.text !== undefined) {
|
|
245
254
|
const bounded = evidence.text.slice(0, TEXT_BOUND);
|
|
246
255
|
if (classifyText(bounded) === "quota_exhausted" && !RATE_LIMIT_TEXT.test(bounded)) {
|
|
@@ -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// 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"]}
|
|
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 === 402) {\n return \"quota_exhausted\"\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 // Payment Required is the account's balance, not one model's limit:\n // every model behind the provider ID (a gateway such as OpenRouter or\n // Ofox) bills the same account, so a sibling model cannot help.\n if (evidence.status === 402) {\n return classification(category, \"status\", true, retryAfterMs, \"provider\")\n }\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"]}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin";
|
|
2
2
|
import { isCodexHeaderName } from "./adapters/codex-capacity/index.js";
|
|
3
|
-
import { adaptiveCommand, escalationTool, registerSessionHooks, subscribeSessions, tierTool, } from "./adapters/opencode/index.js";
|
|
3
|
+
import { adaptiveCommand, escalationTool, processLedger, registerSessionHooks, subscribeSessions, tierTool, unrefTimeout, } from "./adapters/opencode/index.js";
|
|
4
4
|
import { PLUGIN_ID } from "./plugin-id.js";
|
|
5
5
|
import { adaptiveRpc } from "./rpc/definition.js";
|
|
6
6
|
import { adaptiveRpcHandlers, patchSettings } from "./rpc/handlers.js";
|
|
@@ -21,6 +21,34 @@ export default Plugin.define({
|
|
|
21
21
|
model: context.model,
|
|
22
22
|
storage: context.storage,
|
|
23
23
|
clock: () => Date.now(),
|
|
24
|
+
schedule: unrefTimeout,
|
|
25
|
+
ledger: processLedger(),
|
|
26
|
+
});
|
|
27
|
+
// Every disposal settles independently: a failing dispose is logged, never
|
|
28
|
+
// allowed to mask the other disposals or (during rollback) the original
|
|
29
|
+
// setup error. Disposals are awaited so a late rejection cannot land after
|
|
30
|
+
// the host considers the plugin gone.
|
|
31
|
+
const disposals = [];
|
|
32
|
+
const settled = async (name, dispose) => {
|
|
33
|
+
try {
|
|
34
|
+
await dispose();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logFailOpen(name, error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const settledAll = async () => {
|
|
41
|
+
for (const { name, dispose } of disposals) {
|
|
42
|
+
await settled(name, dispose);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// The supervisor arms its parked-work wake on construction, so it is
|
|
46
|
+
// disposed even when a later setup step throws.
|
|
47
|
+
disposals.push({
|
|
48
|
+
name: "wake",
|
|
49
|
+
dispose: () => {
|
|
50
|
+
supervisor.dispose();
|
|
51
|
+
},
|
|
24
52
|
});
|
|
25
53
|
const hooks = await registerSessionHooks(context.session, {
|
|
26
54
|
onRetry: async (input) => {
|
|
@@ -38,30 +66,15 @@ export default Plugin.define({
|
|
|
38
66
|
onPrompt: async (report) => {
|
|
39
67
|
await supervisor.handleReport(report);
|
|
40
68
|
},
|
|
41
|
-
}, { capture: isCodexHeaderName })
|
|
69
|
+
}, { capture: isCodexHeaderName }).catch(async (error) => {
|
|
70
|
+
await settledAll();
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
42
73
|
const subscription = subscribeSessions(context.event, (report) => {
|
|
43
74
|
void supervisor.handleReport(report);
|
|
44
75
|
}, (error) => {
|
|
45
76
|
logFailOpen("events", error);
|
|
46
77
|
});
|
|
47
|
-
// Every disposal settles independently: a failing dispose is logged, never
|
|
48
|
-
// allowed to mask the other disposals or (during rollback) the original
|
|
49
|
-
// setup error. Disposals are awaited so a late rejection cannot land after
|
|
50
|
-
// the host considers the plugin gone.
|
|
51
|
-
const disposals = [];
|
|
52
|
-
const settled = async (name, dispose) => {
|
|
53
|
-
try {
|
|
54
|
-
await dispose();
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
logFailOpen(name, error);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
const settledAll = async () => {
|
|
61
|
-
for (const { name, dispose } of disposals) {
|
|
62
|
-
await settled(name, dispose);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
78
|
for (const [index, hookRegistration] of hooks.entries()) {
|
|
66
79
|
disposals.push({ name: `hook ${String(index)}`, dispose: () => hookRegistration.dispose() });
|
|
67
80
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { Plugin } from \"@opencode/plugin\"\nimport { isCodexHeaderName } from \"./adapters/codex-capacity/index.js\"\nimport {\n adaptiveCommand,\n escalationTool,\n registerSessionHooks,\n subscribeSessions,\n tierTool,\n} from \"./adapters/opencode/index.js\"\nimport { PLUGIN_ID } from \"./plugin-id.js\"\nimport { adaptiveRpc } from \"./rpc/definition.js\"\nimport { adaptiveRpcHandlers, patchSettings } from \"./rpc/handlers.js\"\nimport type { RpcRegistration } from \"@opencode/plugin/promise/rpc\"\nimport type { AdaptiveRpcEmit } from \"./rpc/handlers.js\"\nimport { Supervisor, logFailOpen } from \"./supervisor/index.js\"\n\nexport { PLUGIN_ID } from \"./plugin-id.js\"\n\n/**\n * Entry point wired into OpenCode via `opencode.json(c)`. Setup glues the\n * adapter touchpoints (session hooks, event subscription, storage) to the\n * supervisor and returns a cleanup that disposes every registration.\n * All supervision errors are fail-open (ADP-031): the supervisor's public\n * entry points never throw; the event pump's stream errors are logged.\n */\nexport default Plugin.define({\n id: PLUGIN_ID,\n async setup(context) {\n const supervisor = new Supervisor({\n session: context.session,\n model: context.model,\n storage: context.storage,\n clock: () => Date.now(),\n })\n const hooks = await registerSessionHooks(\n context.session,\n {\n onRetry: async (input) => {\n await supervisor.handleRetry(input)\n },\n onModelRequest: async (report) => {\n await supervisor.handleReport(report)\n },\n onHttpResponse: async (report) => {\n await supervisor.handleReport(report)\n },\n onIdle: async (report) => {\n await supervisor.handleReport(report)\n },\n onPrompt: async (report) => {\n await supervisor.handleReport(report)\n },\n },\n { capture: isCodexHeaderName },\n )\n const subscription = subscribeSessions(\n context.event,\n (report) => {\n void supervisor.handleReport(report)\n },\n (error) => {\n logFailOpen(\"events\", error)\n },\n )\n // Every disposal settles independently: a failing dispose is logged, never\n // allowed to mask the other disposals or (during rollback) the original\n // setup error. Disposals are awaited so a late rejection cannot land after\n // the host considers the plugin gone.\n const disposals: { name: string; dispose: () => void | Promise<void> }[] = []\n const settled = async (name: string, dispose: () => void | Promise<void>): Promise<void> => {\n try {\n await dispose()\n } catch (error) {\n logFailOpen(name, error)\n }\n }\n const settledAll = async (): Promise<void> => {\n for (const { name, dispose } of disposals) {\n await settled(name, dispose)\n }\n }\n for (const [index, hookRegistration] of hooks.entries()) {\n disposals.push({ name: `hook ${String(index)}`, dispose: () => hookRegistration.dispose() })\n }\n disposals.push({\n name: \"events\",\n dispose: () => {\n subscription.dispose()\n },\n })\n // The typed escalation tool (T-18, ADP-015): the agent's self-report\n // surface for stronger capability, applied at the next safe boundary.\n // The tier tool (ADP-055): the agent declares the task's tier; the\n // declaration steers the session's next admissions until cleared.\n // Typed RPC (ADP-034/036): the only channel to the control surfaces.\n // The settings-changed emitter resolves the registration lazily: handlers\n // only run after `register` resolves, so the holder is always populated.\n const rpcHolder: { registration?: RpcRegistration<typeof adaptiveRpc> } = {}\n const emit: AdaptiveRpcEmit = (...args) =>\n // The correlated tuple pairing is checked by the handler types; the\n // registration forwards it to the host emitter untouched.\n rpcHolder.registration?.events.emit(...args)\n try {\n const toolRegistration = await context.tool.transform((editor) => {\n editor.add(\n escalationTool((sessionID, reason, evidence) =>\n supervisor.noteEscalationRequest(sessionID, reason, evidence),\n ),\n )\n editor.add(tierTool((sessionID, tier) => supervisor.noteTierRequest(sessionID, tier)))\n })\n disposals.push({ name: \"tools\", dispose: () => toolRegistration.dispose() })\n const rpcRegistration = await context.rpc.register(\n adaptiveRpc,\n adaptiveRpcHandlers(supervisor, emit),\n )\n rpcHolder.registration = rpcRegistration\n disposals.push({ name: \"rpc\", dispose: () => rpcRegistration.dispose() })\n // The web UI never mounts plugin GUI bundles, but it lists server\n // commands in the composer's slash menu (ADP-034).\n const commandRegistration = await context.command.transform((editor) => {\n editor.add(\n adaptiveCommand(\n {\n status: () => supervisor.status(),\n setMode: (mode) => patchSettings(supervisor, emit, { mode }),\n },\n // The web timeline shows a synthetic message as a notice only when\n // it has a description, and labels it with that description.\n (sessionID, text) =>\n context.session.synthetic({ sessionID, text, description: text, resume: false }),\n ),\n )\n })\n disposals.push({ name: \"command\", dispose: () => commandRegistration.dispose() })\n } catch (error) {\n // Roll back everything already registered so a failure at the tool or\n // rpc stage does not leak live hooks and the event pump.\n await settledAll()\n throw error\n }\n return () => settledAll()\n },\n})\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { Plugin } from \"@opencode/plugin\"\nimport { isCodexHeaderName } from \"./adapters/codex-capacity/index.js\"\nimport {\n adaptiveCommand,\n escalationTool,\n processLedger,\n registerSessionHooks,\n subscribeSessions,\n tierTool,\n unrefTimeout,\n} from \"./adapters/opencode/index.js\"\nimport { PLUGIN_ID } from \"./plugin-id.js\"\nimport { adaptiveRpc } from \"./rpc/definition.js\"\nimport { adaptiveRpcHandlers, patchSettings } from \"./rpc/handlers.js\"\nimport type { RpcRegistration } from \"@opencode/plugin/promise/rpc\"\nimport type { AdaptiveRpcEmit } from \"./rpc/handlers.js\"\nimport { Supervisor, logFailOpen } from \"./supervisor/index.js\"\n\nexport { PLUGIN_ID } from \"./plugin-id.js\"\n\n/**\n * Entry point wired into OpenCode via `opencode.json(c)`. Setup glues the\n * adapter touchpoints (session hooks, event subscription, storage) to the\n * supervisor and returns a cleanup that disposes every registration.\n * All supervision errors are fail-open (ADP-031): the supervisor's public\n * entry points never throw; the event pump's stream errors are logged.\n */\nexport default Plugin.define({\n id: PLUGIN_ID,\n async setup(context) {\n const supervisor = new Supervisor({\n session: context.session,\n model: context.model,\n storage: context.storage,\n clock: () => Date.now(),\n schedule: unrefTimeout,\n ledger: processLedger(),\n })\n // Every disposal settles independently: a failing dispose is logged, never\n // allowed to mask the other disposals or (during rollback) the original\n // setup error. Disposals are awaited so a late rejection cannot land after\n // the host considers the plugin gone.\n const disposals: { name: string; dispose: () => void | Promise<void> }[] = []\n const settled = async (name: string, dispose: () => void | Promise<void>): Promise<void> => {\n try {\n await dispose()\n } catch (error) {\n logFailOpen(name, error)\n }\n }\n const settledAll = async (): Promise<void> => {\n for (const { name, dispose } of disposals) {\n await settled(name, dispose)\n }\n }\n // The supervisor arms its parked-work wake on construction, so it is\n // disposed even when a later setup step throws.\n disposals.push({\n name: \"wake\",\n dispose: () => {\n supervisor.dispose()\n },\n })\n const hooks = await registerSessionHooks(\n context.session,\n {\n onRetry: async (input) => {\n await supervisor.handleRetry(input)\n },\n onModelRequest: async (report) => {\n await supervisor.handleReport(report)\n },\n onHttpResponse: async (report) => {\n await supervisor.handleReport(report)\n },\n onIdle: async (report) => {\n await supervisor.handleReport(report)\n },\n onPrompt: async (report) => {\n await supervisor.handleReport(report)\n },\n },\n { capture: isCodexHeaderName },\n ).catch(async (error: unknown) => {\n await settledAll()\n throw error\n })\n const subscription = subscribeSessions(\n context.event,\n (report) => {\n void supervisor.handleReport(report)\n },\n (error) => {\n logFailOpen(\"events\", error)\n },\n )\n for (const [index, hookRegistration] of hooks.entries()) {\n disposals.push({ name: `hook ${String(index)}`, dispose: () => hookRegistration.dispose() })\n }\n disposals.push({\n name: \"events\",\n dispose: () => {\n subscription.dispose()\n },\n })\n // The typed escalation tool (T-18, ADP-015): the agent's self-report\n // surface for stronger capability, applied at the next safe boundary.\n // The tier tool (ADP-055): the agent declares the task's tier; the\n // declaration steers the session's next admissions until cleared.\n // Typed RPC (ADP-034/036): the only channel to the control surfaces.\n // The settings-changed emitter resolves the registration lazily: handlers\n // only run after `register` resolves, so the holder is always populated.\n const rpcHolder: { registration?: RpcRegistration<typeof adaptiveRpc> } = {}\n const emit: AdaptiveRpcEmit = (...args) =>\n // The correlated tuple pairing is checked by the handler types; the\n // registration forwards it to the host emitter untouched.\n rpcHolder.registration?.events.emit(...args)\n try {\n const toolRegistration = await context.tool.transform((editor) => {\n editor.add(\n escalationTool((sessionID, reason, evidence) =>\n supervisor.noteEscalationRequest(sessionID, reason, evidence),\n ),\n )\n editor.add(tierTool((sessionID, tier) => supervisor.noteTierRequest(sessionID, tier)))\n })\n disposals.push({ name: \"tools\", dispose: () => toolRegistration.dispose() })\n const rpcRegistration = await context.rpc.register(\n adaptiveRpc,\n adaptiveRpcHandlers(supervisor, emit),\n )\n rpcHolder.registration = rpcRegistration\n disposals.push({ name: \"rpc\", dispose: () => rpcRegistration.dispose() })\n // The web UI never mounts plugin GUI bundles, but it lists server\n // commands in the composer's slash menu (ADP-034).\n const commandRegistration = await context.command.transform((editor) => {\n editor.add(\n adaptiveCommand(\n {\n status: () => supervisor.status(),\n setMode: (mode) => patchSettings(supervisor, emit, { mode }),\n },\n // The web timeline shows a synthetic message as a notice only when\n // it has a description, and labels it with that description.\n (sessionID, text) =>\n context.session.synthetic({ sessionID, text, description: text, resume: false }),\n ),\n )\n })\n disposals.push({ name: \"command\", dispose: () => commandRegistration.dispose() })\n } catch (error) {\n // Roll back everything already registered so a failure at the tool or\n // rpc stage does not leak live hooks and the event pump.\n await settledAll()\n throw error\n }\n return () => settledAll()\n },\n})\n"]}
|
package/dist/rpc/status.d.ts
CHANGED
|
@@ -26,11 +26,19 @@ export interface TierResourceStatus {
|
|
|
26
26
|
/** Wall-clock capacity reset deadline (ms epoch); null when unread. */
|
|
27
27
|
resetAt: number | null;
|
|
28
28
|
}
|
|
29
|
+
/** A session whose parked work waits for its tier to recover (ADP-024). */
|
|
30
|
+
export interface WaitingSession {
|
|
31
|
+
sessionID: string;
|
|
32
|
+
tier: TierName;
|
|
33
|
+
/** When the wake resumes it (ms epoch); null when no resource states a reset. */
|
|
34
|
+
resumeAt: number | null;
|
|
35
|
+
}
|
|
29
36
|
export interface StatusSnapshot {
|
|
30
37
|
mode: OperatingMode;
|
|
31
38
|
pin: ModelRef | null;
|
|
32
39
|
tiers: Readonly<Record<TierName, readonly TierResourceStatus[]>>;
|
|
33
40
|
parked: number;
|
|
41
|
+
waiting: readonly WaitingSession[];
|
|
34
42
|
receipts: number;
|
|
35
43
|
}
|
|
36
44
|
export declare function buildStatus(input: {
|
|
@@ -38,5 +46,6 @@ export declare function buildStatus(input: {
|
|
|
38
46
|
health: readonly ResourceRecord[];
|
|
39
47
|
capacity: Readonly<Record<string, CapacityReading | null>>;
|
|
40
48
|
parked: number;
|
|
49
|
+
waiting: readonly WaitingSession[];
|
|
41
50
|
receipts: number;
|
|
42
51
|
}): StatusSnapshot;
|
package/dist/rpc/status.js
CHANGED
package/dist/rpc/status.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/rpc/status.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAG/D,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AAGpD,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAA;
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/rpc/status.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAG/D,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AAGpD,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAA;AAsClE,MAAM,UAAU,WAAW,CAAC,KAO3B;IACC,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAA;IAC/C,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,MAAM;QAAE,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAA;IAC/E,MAAM,KAAK,GAAG,EAA4C,CAAA;IAC1D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YACnD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAA;YACjD,MAAM,eAAe,GAAG,KAAK,CAAC,QAAQ,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAA;YACvE,OAAO;gBACL,GAAG;gBACH,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,SAAS;gBACnC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,QAAQ;gBAC1C,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS;gBACtC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,IAAI;gBAC1C,WAAW,EAAE,mBAAmB,CAAC,eAAe,CAAC;gBACjD,OAAO,EAAE,eAAe,EAAE,OAAO,IAAI,IAAI;aAC1C,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI;QACzB,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG;QACvB,KAAK;QACL,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,mBAAmB,CAAC,OAA+B;IAC1D,IAAI,OAAO,EAAE,SAAS,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,GAAG,GAAG,CAAC,CAAA;IAChF,IAAI,OAAO,EAAE,cAAc,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG,GAAG,CAAC,CAAA;IAChG,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["/**\n * Status snapshot for the control surfaces (ADP-033): a bounded, secret-free\n * view of the settings and the resource-health state of every configured\n * resource. Pure — the supervisor (or any future caller) gathers the inputs.\n */\nimport type { OperatingMode } from \"../core/scheduler/types.js\"\nimport { schedulerHealthKey } from \"../core/scheduler/index.js\"\nimport type { ModelRef } from \"../core/work-unit/types.js\"\nimport type { TierName } from \"../core/tier-policy/types.js\"\nimport { TIERS } from \"../core/tier-policy/types.js\"\nimport type { ResourceRecord } from \"../health/resource-health/types.js\"\nimport type { CapacityReading } from \"../health/capacity/types.js\"\nimport { resourceKeyID } from \"../health/resource-health/index.js\"\nimport type { SettingsV1 } from \"../state/settings/index.js\"\n\nexport interface TierResourceStatus {\n ref: ModelRef\n health: ResourceRecord[\"health\"]\n breaker: ResourceRecord[\"breaker\"][\"state\"]\n windowed: boolean\n /**\n * Wall-clock deadline of the active window (ms epoch); null when unwindowed.\n * Null — not undefined: the host RPC encoder rejects JSON objects carrying a\n * present-but-undefined key (\"Expected JSON value\"), which 500'd every\n * status.get once any resource was configured.\n */\n windowUntil: number | null\n /** Used capacity as a rounded whole percent; null when no reading exists. */\n usedPercent: number | null\n /** Wall-clock capacity reset deadline (ms epoch); null when unread. */\n resetAt: number | null\n}\n\n/** A session whose parked work waits for its tier to recover (ADP-024). */\nexport interface WaitingSession {\n sessionID: string\n tier: TierName\n /** When the wake resumes it (ms epoch); null when no resource states a reset. */\n resumeAt: number | null\n}\n\nexport interface StatusSnapshot {\n mode: OperatingMode\n pin: ModelRef | null\n tiers: Readonly<Record<TierName, readonly TierResourceStatus[]>>\n parked: number\n waiting: readonly WaitingSession[]\n receipts: number\n}\n\nexport function buildStatus(input: {\n settings: SettingsV1\n health: readonly ResourceRecord[]\n capacity: Readonly<Record<string, CapacityReading | null>>\n parked: number\n waiting: readonly WaitingSession[]\n receipts: number\n}): StatusSnapshot {\n const byKey = new Map<string, ResourceRecord>()\n for (const record of input.health) byKey.set(resourceKeyID(record.key), record)\n const tiers = {} as Record<TierName, TierResourceStatus[]>\n for (const tier of TIERS) {\n tiers[tier] = input.settings.tiers[tier].map((ref) => {\n const record = byKey.get(schedulerHealthKey(ref))\n const capacityReading = input.capacity[schedulerHealthKey(ref)] ?? null\n return {\n ref,\n health: record?.health ?? \"healthy\",\n breaker: record?.breaker.state ?? \"closed\",\n windowed: record?.window !== undefined,\n windowUntil: record?.window?.until ?? null,\n usedPercent: capacityUsedPercent(capacityReading),\n resetAt: capacityReading?.resetAt ?? null,\n }\n })\n }\n return {\n mode: input.settings.mode,\n pin: input.settings.pin,\n tiers,\n parked: input.parked,\n waiting: input.waiting,\n receipts: input.receipts,\n }\n}\n\n/**\n * Derives the used-capacity percentage from a reading. Readings may carry any\n * subset of the two ratios: usedRatio directly, or the remainder inferred from\n * remainingRatio. A reading with neither ratio is still present (confidence,\n * resetAt may be set) but yields no percentage — null, not undefined.\n */\nfunction capacityUsedPercent(reading: CapacityReading | null): number | null {\n if (reading?.usedRatio !== undefined) return Math.round(reading.usedRatio * 100)\n if (reading?.remainingRatio !== undefined) return Math.round((1 - reading.remainingRatio) * 100)\n return null\n}\n"]}
|