@cortexkit/opencode-openai-auth 0.4.1 → 0.4.3

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 (37) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/core/oauth.d.ts +1 -1
  3. package/dist/core/provider.d.ts +11 -0
  4. package/dist/core/quota-manager.d.ts +7 -11
  5. package/dist/core/refresh-all-quota.d.ts +10 -0
  6. package/dist/index-9k2gxqc1.js +89 -0
  7. package/dist/{index-1anzb8jh.js → index-ptsh66z7.js} +1 -1
  8. package/dist/{index-b6kp9ydc.js → index-rckqt97a.js} +1 -1
  9. package/dist/index-reagbdg3.js +2 -0
  10. package/dist/{index-6y6ss1kc.js → index-xbq3aq6f.js} +1 -1
  11. package/dist/{index-y67qn95a.js → index-xcf8tm40.js} +1 -1
  12. package/dist/index-ya6hw803.js +7 -0
  13. package/dist/index.js +69 -64
  14. package/dist/{quota-normalize-8yzxqy78.js → quota-normalize-754rewnb.js} +1 -1
  15. package/dist/raw-ws-bun.d.ts +2 -0
  16. package/dist/raw-ws-node.d.ts +2 -0
  17. package/dist/raw-ws-upgrade.d.ts +2 -0
  18. package/dist/rpc/port-file.js +1 -1
  19. package/dist/rpc/rpc-client.js +1 -1
  20. package/dist/rpc/rpc-dir.js +1 -1
  21. package/dist/sidebar-state.d.ts +2 -0
  22. package/dist/sidebar-state.js +1 -1
  23. package/dist/token-util-xzavn760.js +1 -0
  24. package/dist/{token-yzcb1p3e.js → token-xsc39hv8.js} +1 -1
  25. package/dist/tui-preferences.js +1 -1
  26. package/dist/version.d.ts +1 -1
  27. package/dist/ws-pool.d.ts +2 -11
  28. package/dist/ws.d.ts +7 -2
  29. package/package.json +4 -4
  30. package/src/core/refresh-file-lock.ts +16 -0
  31. package/src/sidebar-state.ts +34 -3
  32. package/src/tui-compiled/core/refresh-file-lock.ts +309 -0
  33. package/src/tui-compiled/sidebar-state.ts +34 -3
  34. package/dist/index-0gccjqxr.js +0 -89
  35. package/dist/index-1r02s323.js +0 -6
  36. package/dist/index-fbfpefx6.js +0 -2
  37. package/dist/token-util-h46jb3eg.js +0 -1
@@ -1 +1 @@
1
- import{c as a,d as b,e as c,f as d,g as e}from"./index-2ng03cby.js";import"./index-6y6ss1kc.js";export{a as toResetIso,d as normalizeWsFrame,e as normalizeWham,b as normalizeQuotaHeaders,c as isCompleteQuotaHeaderFrame};
1
+ import{c as a,d as b,e as c,f as d,g as e}from"./index-2ng03cby.js";import"./index-xbq3aq6f.js";export{a as toResetIso,d as normalizeWsFrame,e as normalizeWham,b as normalizeQuotaHeaders,c as isCompleteQuotaHeaderFrame};
@@ -7,6 +7,7 @@ export declare class RawWebSocket {
7
7
  private listeners;
8
8
  private rxBuffer;
9
9
  private handshakeDone;
10
+ private upgradeFailureEmitted;
10
11
  private expectedAccept;
11
12
  private fragOpcode;
12
13
  private fragChunks;
@@ -21,6 +22,7 @@ export declare class RawWebSocket {
21
22
  private emit;
22
23
  private connect;
23
24
  private onData;
25
+ private emitRejectedUpgrade;
24
26
  private drainFrames;
25
27
  private handleFrame;
26
28
  send(data: string): void;
@@ -7,6 +7,7 @@ export declare class RawWebSocket {
7
7
  private listeners;
8
8
  private rxBuffer;
9
9
  private handshakeDone;
10
+ private upgradeFailureEmitted;
10
11
  private expectedAccept;
11
12
  private fragOpcode;
12
13
  private fragChunks;
@@ -20,6 +21,7 @@ export declare class RawWebSocket {
20
21
  private emit;
21
22
  private connect;
22
23
  private onData;
24
+ private emitRejectedUpgrade;
23
25
  private drainFrames;
24
26
  private handleFrame;
25
27
  send(data: string): void;
@@ -0,0 +1,2 @@
1
+ export declare function rejectedUpgradeStatus(statusLine: string): number | undefined;
2
+ export declare function rejectedUpgradeEvent(buffer: Uint8Array, headerEnd: number, finalizePartial?: boolean): Record<string, unknown> | undefined;
@@ -1 +1 @@
1
- import{ma as a,na as b}from"../index-1anzb8jh.js";import"../index-6y6ss1kc.js";export{a as writePortFile,b as discoverPortFile};
1
+ import{oa as a,pa as b}from"../index-ptsh66z7.js";import"../index-xbq3aq6f.js";export{a as writePortFile,b as discoverPortFile};
@@ -1 +1 @@
1
- import{na as c}from"../index-1anzb8jh.js";import"../index-6y6ss1kc.js";async function p(n,r,l,o,i){let t=await c(n,r);if(l?.(t),!t)return null;let e=new AbortController,s=setTimeout(()=>e.abort(),2000);try{let a=await fetch(`http://127.0.0.1:${t.port}/rpc/${o}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${t.token}`},body:JSON.stringify(i),signal:e.signal});if(!a.ok)return null;return await a.json()}catch{return null}finally{clearTimeout(s)}}function y(n,r,l){let o=!1,i=(t)=>{if(o)return;o=!0,l?.(t)};return{async pending(t,e){return(await p(n,r,i,"pending-notifications",{lastReceivedId:t,sessionId:e}))?.messages??[]},async apply(t){return await p(n,r,i,"apply",{...t})??{text:"apply failed",knobs:{}}}}}export{y as createRpcClient};
1
+ import{pa as c}from"../index-ptsh66z7.js";import"../index-xbq3aq6f.js";async function p(n,r,l,o,i){let t=await c(n,r);if(l?.(t),!t)return null;let e=new AbortController,s=setTimeout(()=>e.abort(),2000);try{let a=await fetch(`http://127.0.0.1:${t.port}/rpc/${o}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${t.token}`},body:JSON.stringify(i),signal:e.signal});if(!a.ok)return null;return await a.json()}catch{return null}finally{clearTimeout(s)}}function y(n,r,l){let o=!1,i=(t)=>{if(o)return;o=!0,l?.(t)};return{async pending(t,e){return(await p(n,r,i,"pending-notifications",{lastReceivedId:t,sessionId:e}))?.messages??[]},async apply(t){return await p(n,r,i,"apply",{...t})??{text:"apply failed",knobs:{}}}}}export{y as createRpcClient};
@@ -1 +1 @@
1
- import{oa as a,pa as b}from"../index-b6kp9ydc.js";import"../index-6y6ss1kc.js";export{a as tmpdir,b as getRpcDir};
1
+ import{qa as a,ra as b}from"../index-rckqt97a.js";import"../index-xbq3aq6f.js";export{a as tmpdir,b as getRpcDir};
@@ -70,8 +70,10 @@ export declare const ACTIVE_ROUTING_MAX_ENTRIES = 128;
70
70
  export type SidebarRoutingAccount = {
71
71
  id: string;
72
72
  enabled?: boolean;
73
+ killed?: boolean;
73
74
  };
74
75
  export declare function isUsableRoutingEntry(entry: ActiveRoutingEntry, accounts: readonly SidebarRoutingAccount[], now?: number): boolean;
76
+ export declare function isQuotaExhausted(quota: AccountQuota | null | undefined, now?: number): boolean;
75
77
  export declare function resolveSessionSidebarRouting(state: SidebarState, sessionId?: string, now?: number): {
76
78
  activeId: string;
77
79
  route: string;
@@ -1 +1 @@
1
- import{$ as l,Q as a,R as b,S as c,T as d,U as e,V as f,W as g,X as h,Y as i,Z as j,_ as k,aa as m,ba as n,ca as o,da as p,ea as q,fa as r,ga as s,ha as t}from"./index-fbfpefx6.js";import"./index-1r02s323.js";import"./index-6y6ss1kc.js";export{n as upsertSidebarActiveRouting,l as setSidebarState,m as setSidebarMachineState,o as setSidebarLegacyRouting,j as resolveSessionSidebarRouting,r as resolveActiveAccount,p as removeSidebarActiveRouting,k as pruneActiveRouting,e as normalizeSidebarState,i as isUsableRoutingEntry,c as getSidebarStateFile,f as getSidebarState,b as getPresentQuotaWindows,s as getCollapsedQuotaSummary,a as formatWindowLabel,q as drainSidebarWrites,t as computeQuotaPacing,d as DEFAULT_SIDEBAR_STATE,h as ACTIVE_ROUTING_MAX_ENTRIES,g as ACTIVE_ROUTING_MAX_AGE_MS};
1
+ import{$ as k,R as a,S as b,T as c,U as d,V as e,W as f,X as g,Y as h,Z as i,_ as j,aa as l,ba as m,ca as n,da as o,ea as p,fa as q,ga as r,ha as s,ia as t,ja as u}from"./index-reagbdg3.js";import"./index-ya6hw803.js";import"./index-xbq3aq6f.js";export{o as upsertSidebarActiveRouting,m as setSidebarState,n as setSidebarMachineState,p as setSidebarLegacyRouting,k as resolveSessionSidebarRouting,s as resolveActiveAccount,q as removeSidebarActiveRouting,l as pruneActiveRouting,e as normalizeSidebarState,i as isUsableRoutingEntry,j as isQuotaExhausted,c as getSidebarStateFile,f as getSidebarState,b as getPresentQuotaWindows,t as getCollapsedQuotaSummary,a as formatWindowLabel,r as drainSidebarWrites,u as computeQuotaPacing,d as DEFAULT_SIDEBAR_STATE,h as ACTIVE_ROUTING_MAX_ENTRIES,g as ACTIVE_ROUTING_MAX_AGE_MS};
@@ -0,0 +1 @@
1
+ import{b as a}from"./index-xcf8tm40.js";import"./index-xbq3aq6f.js";export default a();
@@ -1 +1 @@
1
- import{a as U,b as V}from"./index-y67qn95a.js";import{ra as R}from"./index-6y6ss1kc.js";var E=R((M,Q)=>{var{defineProperty:H,getOwnPropertyDescriptor:W,getOwnPropertyNames:X}=Object,Y=Object.prototype.hasOwnProperty,Z=(z,q)=>{for(var B in q)H(z,B,{get:q[B],enumerable:!0})},$=(z,q,B,F)=>{if(q&&typeof q==="object"||typeof q==="function"){for(let v of X(q))if(!Y.call(z,v)&&v!==B)H(z,v,{get:()=>q[v],enumerable:!(F=W(q,v))||F.enumerable})}return z},A=(z)=>$(H({},"__esModule",{value:!0}),z),L={};Z(L,{refreshToken:()=>D});Q.exports=A(L);var K=U(),G=V();async function D(z){let q=z?.project,B=z?.team;if(!q&&!B){let v=(0,G.findProjectInfo)();q=v.projectId,B=v.teamId}else if(!q||!B){let v=(0,G.findProjectInfo)();q=q??v.projectId,B=B??v.teamId}if(!q)throw new K.VercelOidcTokenError("Failed to refresh OIDC token: No project specified. Try re-linking your project with `vc link`");let F=(0,G.loadToken)(q);if(!F||(0,G.isExpired)((0,G.getTokenPayload)(F.token),z?.expirationBufferMs)){let v=await(0,G.getVercelToken)({expirationBufferMs:z?.expirationBufferMs});if(F=await(0,G.getVercelOidcToken)(v,q,B),!F)throw new K.VercelOidcTokenError("Failed to refresh OIDC token");(0,G.saveToken)(F,q)}process.env.VERCEL_OIDC_TOKEN=F.token;return}});export default E();
1
+ import{a as U,b as V}from"./index-xcf8tm40.js";import{ta as R}from"./index-xbq3aq6f.js";var E=R((M,Q)=>{var{defineProperty:H,getOwnPropertyDescriptor:W,getOwnPropertyNames:X}=Object,Y=Object.prototype.hasOwnProperty,Z=(z,q)=>{for(var B in q)H(z,B,{get:q[B],enumerable:!0})},$=(z,q,B,F)=>{if(q&&typeof q==="object"||typeof q==="function"){for(let v of X(q))if(!Y.call(z,v)&&v!==B)H(z,v,{get:()=>q[v],enumerable:!(F=W(q,v))||F.enumerable})}return z},A=(z)=>$(H({},"__esModule",{value:!0}),z),L={};Z(L,{refreshToken:()=>D});Q.exports=A(L);var K=U(),G=V();async function D(z){let q=z?.project,B=z?.team;if(!q&&!B){let v=(0,G.findProjectInfo)();q=v.projectId,B=v.teamId}else if(!q||!B){let v=(0,G.findProjectInfo)();q=q??v.projectId,B=B??v.teamId}if(!q)throw new K.VercelOidcTokenError("Failed to refresh OIDC token: No project specified. Try re-linking your project with `vc link`");let F=(0,G.loadToken)(q);if(!F||(0,G.isExpired)((0,G.getTokenPayload)(F.token),z?.expirationBufferMs)){let v=await(0,G.getVercelToken)({expirationBufferMs:z?.expirationBufferMs});if(F=await(0,G.getVercelOidcToken)(v,q,B),!F)throw new K.VercelOidcTokenError("Failed to refresh OIDC token");(0,G.saveToken)(F,q)}process.env.VERCEL_OIDC_TOKEN=F.token;return}});export default E();
@@ -1,4 +1,4 @@
1
- import{qa as P,ra as C,ta as _}from"./index-6y6ss1kc.js";var g=C((T,L)=>{(function(Q){if(typeof L==="object"&&typeof L.exports==="object"){var J=Q(_,T);if(J!==void 0)L.exports=J}else if(typeof define==="function"&&define.amd)define(["require","exports","./impl/format","./impl/edit","./impl/scanner","./impl/parser"],Q)})(function(Q,J){Object.defineProperty(J,"__esModule",{value:!0}),J.applyEdits=J.modify=J.format=J.printParseErrorCode=J.ParseErrorCode=J.stripComments=J.visit=J.getNodeValue=J.getNodePath=J.findNodeAtOffset=J.findNodeAtLocation=J.parseTree=J.parse=J.getLocation=J.SyntaxKind=J.ScanError=J.createScanner=void 0;let X=Q("./impl/format"),Z=Q("./impl/edit"),G=Q("./impl/scanner"),$=Q("./impl/parser");J.createScanner=G.createScanner;var W;(function(z){z[z.None=0]="None",z[z.UnexpectedEndOfComment=1]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=2]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=3]="UnexpectedEndOfNumber",z[z.InvalidUnicode=4]="InvalidUnicode",z[z.InvalidEscapeCharacter=5]="InvalidEscapeCharacter",z[z.InvalidCharacter=6]="InvalidCharacter"})(W||(J.ScanError=W={}));var H;(function(z){z[z.OpenBraceToken=1]="OpenBraceToken",z[z.CloseBraceToken=2]="CloseBraceToken",z[z.OpenBracketToken=3]="OpenBracketToken",z[z.CloseBracketToken=4]="CloseBracketToken",z[z.CommaToken=5]="CommaToken",z[z.ColonToken=6]="ColonToken",z[z.NullKeyword=7]="NullKeyword",z[z.TrueKeyword=8]="TrueKeyword",z[z.FalseKeyword=9]="FalseKeyword",z[z.StringLiteral=10]="StringLiteral",z[z.NumericLiteral=11]="NumericLiteral",z[z.LineCommentTrivia=12]="LineCommentTrivia",z[z.BlockCommentTrivia=13]="BlockCommentTrivia",z[z.LineBreakTrivia=14]="LineBreakTrivia",z[z.Trivia=15]="Trivia",z[z.Unknown=16]="Unknown",z[z.EOF=17]="EOF"})(H||(J.SyntaxKind=H={})),J.getLocation=$.getLocation,J.parse=$.parse,J.parseTree=$.parseTree,J.findNodeAtLocation=$.findNodeAtLocation,J.findNodeAtOffset=$.findNodeAtOffset,J.getNodePath=$.getNodePath,J.getNodeValue=$.getNodeValue,J.visit=$.visit,J.stripComments=$.stripComments;var q;(function(z){z[z.InvalidSymbol=1]="InvalidSymbol",z[z.InvalidNumberFormat=2]="InvalidNumberFormat",z[z.PropertyNameExpected=3]="PropertyNameExpected",z[z.ValueExpected=4]="ValueExpected",z[z.ColonExpected=5]="ColonExpected",z[z.CommaExpected=6]="CommaExpected",z[z.CloseBraceExpected=7]="CloseBraceExpected",z[z.CloseBracketExpected=8]="CloseBracketExpected",z[z.EndOfFileExpected=9]="EndOfFileExpected",z[z.InvalidCommentToken=10]="InvalidCommentToken",z[z.UnexpectedEndOfComment=11]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=12]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=13]="UnexpectedEndOfNumber",z[z.InvalidUnicode=14]="InvalidUnicode",z[z.InvalidEscapeCharacter=15]="InvalidEscapeCharacter",z[z.InvalidCharacter=16]="InvalidCharacter"})(q||(J.ParseErrorCode=q={}));function D(z){switch(z){case 1:return"InvalidSymbol";case 2:return"InvalidNumberFormat";case 3:return"PropertyNameExpected";case 4:return"ValueExpected";case 5:return"ColonExpected";case 6:return"CommaExpected";case 7:return"CloseBraceExpected";case 8:return"CloseBracketExpected";case 9:return"EndOfFileExpected";case 10:return"InvalidCommentToken";case 11:return"UnexpectedEndOfComment";case 12:return"UnexpectedEndOfString";case 13:return"UnexpectedEndOfNumber";case 14:return"InvalidUnicode";case 15:return"InvalidEscapeCharacter";case 16:return"InvalidCharacter"}return"<unknown ParseErrorCode>"}J.printParseErrorCode=D;function m(z,U,j){return X.format(z,U,j)}J.format=m;function f(z,U,j,A){return Z.setProperty(z,U,j,A)}J.modify=f;function u(z,U){let j=U.slice(0).sort((I,Y)=>{let F=I.offset-Y.offset;if(F===0)return I.length-Y.length;return F}),A=z.length;for(let I=j.length-1;I>=0;I--){let Y=j[I];if(Y.offset+Y.length<=A)z=Z.applyEdit(z,Y);else throw Error("Overlapping edit");A=Y.offset}return z}J.applyEdits=u})});var M=P(g(),1);import{watch as K}from"node:fs";import{mkdir as c,readFile as O,rename as y,writeFile as S}from"node:fs/promises";import{homedir as E}from"node:os";import{basename as l,dirname as v,join as k}from"node:path";var p="OPENCODE_TUI_PREFERENCES_FILE",x="tui-preferences.jsonc";function w(){let Q=process.env[p];if(Q)return Q;let J=process.env.OPENCODE_CONFIG_DIR||k(process.env.XDG_CONFIG_HOME||k(E(),".config"),"opencode");return k(J,x)}function N(Q){return typeof Q==="object"&&Q!==null&&!Array.isArray(Q)}async function Xz(){try{let Q=await O(w(),"utf8"),J=[],X=M.parse(Q,J,{allowTrailingComma:!0});if(J.length>0)return{};return N(X)?X:{}}catch{return{}}}var i="openai-auth",s=160,b={forceToTop:!1,order:s,startCollapsed:!1,rememberCollapsed:!0,collapsed:null,pollMs:1500,refreshDebounceMs:200,header:{label:"OPENAI",showVersion:!0},sections:{quota:!0,fallbackAccounts:!0,routing:!0,health:!0,pacing:!0},appearance:{barWidth:10,barFilledChar:"█",barEmptyChar:"░",warnThreshold:50,errorThreshold:80}};function V(Q,J){return typeof Q==="boolean"?Q:J}function B(Q,J,X,Z){if(typeof Q!=="number"||!Number.isFinite(Q))return J;return Math.min(Math.max(Math.round(Q),X),Z)}function d(Q,J,X){if(typeof Q!=="string"||Q.length===0)return J;return Q.slice(0,X)}function h(Q,J){if(typeof Q!=="string")return J;return[...Q][0]??J}function Zz(Q){let J=Q[i];if(!N(J))return structuredClone(b);let X=b,Z=N(J.header)?J.header:{},G=N(J.sections)?J.sections:{},$=N(J.appearance)?J.appearance:{},W=B($.warnThreshold,X.appearance.warnThreshold,0,99),H=Math.max(B($.errorThreshold,X.appearance.errorThreshold,0,100),Math.min(W+1,100));return{forceToTop:V(J.forceToTop,X.forceToTop),order:B(J.order,X.order,-1e4,1e4),startCollapsed:V(J.startCollapsed,X.startCollapsed),rememberCollapsed:V(J.rememberCollapsed,X.rememberCollapsed),collapsed:typeof J.collapsed==="boolean"?J.collapsed:null,pollMs:B(J.pollMs,X.pollMs,500,30000),refreshDebounceMs:B(J.refreshDebounceMs,X.refreshDebounceMs,50,5000),header:{label:d(Z.label,X.header.label,20),showVersion:V(Z.showVersion,X.header.showVersion)},sections:{quota:V(G.quota,X.sections.quota),fallbackAccounts:V(G.fallbackAccounts,X.sections.fallbackAccounts),routing:V(G.routing,X.sections.routing),health:V(G.health,X.sections.health),pacing:V(G.pacing,X.sections.pacing)},appearance:{barWidth:B($.barWidth,X.appearance.barWidth,4,40),barFilledChar:h($.barFilledChar,X.appearance.barFilledChar),barEmptyChar:h($.barEmptyChar,X.appearance.barEmptyChar),warnThreshold:W,errorThreshold:H}}}var n=-1e5;function $z(Q,J,X){let Z=Q[J];if(!N(Z))return X;if(Z.forceToTop===!0)return n+Object.keys(Q).indexOf(J);return B(Z.order,X,-1e4,1e4)}var t=`// Shared preferences for opencode TUI plugins.
1
+ import{sa as P,ta as C,va as _}from"./index-xbq3aq6f.js";var g=C((T,L)=>{(function(Q){if(typeof L==="object"&&typeof L.exports==="object"){var J=Q(_,T);if(J!==void 0)L.exports=J}else if(typeof define==="function"&&define.amd)define(["require","exports","./impl/format","./impl/edit","./impl/scanner","./impl/parser"],Q)})(function(Q,J){Object.defineProperty(J,"__esModule",{value:!0}),J.applyEdits=J.modify=J.format=J.printParseErrorCode=J.ParseErrorCode=J.stripComments=J.visit=J.getNodeValue=J.getNodePath=J.findNodeAtOffset=J.findNodeAtLocation=J.parseTree=J.parse=J.getLocation=J.SyntaxKind=J.ScanError=J.createScanner=void 0;let X=Q("./impl/format"),Z=Q("./impl/edit"),G=Q("./impl/scanner"),$=Q("./impl/parser");J.createScanner=G.createScanner;var W;(function(z){z[z.None=0]="None",z[z.UnexpectedEndOfComment=1]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=2]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=3]="UnexpectedEndOfNumber",z[z.InvalidUnicode=4]="InvalidUnicode",z[z.InvalidEscapeCharacter=5]="InvalidEscapeCharacter",z[z.InvalidCharacter=6]="InvalidCharacter"})(W||(J.ScanError=W={}));var H;(function(z){z[z.OpenBraceToken=1]="OpenBraceToken",z[z.CloseBraceToken=2]="CloseBraceToken",z[z.OpenBracketToken=3]="OpenBracketToken",z[z.CloseBracketToken=4]="CloseBracketToken",z[z.CommaToken=5]="CommaToken",z[z.ColonToken=6]="ColonToken",z[z.NullKeyword=7]="NullKeyword",z[z.TrueKeyword=8]="TrueKeyword",z[z.FalseKeyword=9]="FalseKeyword",z[z.StringLiteral=10]="StringLiteral",z[z.NumericLiteral=11]="NumericLiteral",z[z.LineCommentTrivia=12]="LineCommentTrivia",z[z.BlockCommentTrivia=13]="BlockCommentTrivia",z[z.LineBreakTrivia=14]="LineBreakTrivia",z[z.Trivia=15]="Trivia",z[z.Unknown=16]="Unknown",z[z.EOF=17]="EOF"})(H||(J.SyntaxKind=H={})),J.getLocation=$.getLocation,J.parse=$.parse,J.parseTree=$.parseTree,J.findNodeAtLocation=$.findNodeAtLocation,J.findNodeAtOffset=$.findNodeAtOffset,J.getNodePath=$.getNodePath,J.getNodeValue=$.getNodeValue,J.visit=$.visit,J.stripComments=$.stripComments;var q;(function(z){z[z.InvalidSymbol=1]="InvalidSymbol",z[z.InvalidNumberFormat=2]="InvalidNumberFormat",z[z.PropertyNameExpected=3]="PropertyNameExpected",z[z.ValueExpected=4]="ValueExpected",z[z.ColonExpected=5]="ColonExpected",z[z.CommaExpected=6]="CommaExpected",z[z.CloseBraceExpected=7]="CloseBraceExpected",z[z.CloseBracketExpected=8]="CloseBracketExpected",z[z.EndOfFileExpected=9]="EndOfFileExpected",z[z.InvalidCommentToken=10]="InvalidCommentToken",z[z.UnexpectedEndOfComment=11]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=12]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=13]="UnexpectedEndOfNumber",z[z.InvalidUnicode=14]="InvalidUnicode",z[z.InvalidEscapeCharacter=15]="InvalidEscapeCharacter",z[z.InvalidCharacter=16]="InvalidCharacter"})(q||(J.ParseErrorCode=q={}));function D(z){switch(z){case 1:return"InvalidSymbol";case 2:return"InvalidNumberFormat";case 3:return"PropertyNameExpected";case 4:return"ValueExpected";case 5:return"ColonExpected";case 6:return"CommaExpected";case 7:return"CloseBraceExpected";case 8:return"CloseBracketExpected";case 9:return"EndOfFileExpected";case 10:return"InvalidCommentToken";case 11:return"UnexpectedEndOfComment";case 12:return"UnexpectedEndOfString";case 13:return"UnexpectedEndOfNumber";case 14:return"InvalidUnicode";case 15:return"InvalidEscapeCharacter";case 16:return"InvalidCharacter"}return"<unknown ParseErrorCode>"}J.printParseErrorCode=D;function m(z,U,j){return X.format(z,U,j)}J.format=m;function f(z,U,j,A){return Z.setProperty(z,U,j,A)}J.modify=f;function u(z,U){let j=U.slice(0).sort((I,Y)=>{let F=I.offset-Y.offset;if(F===0)return I.length-Y.length;return F}),A=z.length;for(let I=j.length-1;I>=0;I--){let Y=j[I];if(Y.offset+Y.length<=A)z=Z.applyEdit(z,Y);else throw Error("Overlapping edit");A=Y.offset}return z}J.applyEdits=u})});var M=P(g(),1);import{watch as K}from"node:fs";import{mkdir as c,readFile as O,rename as y,writeFile as S}from"node:fs/promises";import{homedir as E}from"node:os";import{basename as l,dirname as v,join as k}from"node:path";var p="OPENCODE_TUI_PREFERENCES_FILE",x="tui-preferences.jsonc";function w(){let Q=process.env[p];if(Q)return Q;let J=process.env.OPENCODE_CONFIG_DIR||k(process.env.XDG_CONFIG_HOME||k(E(),".config"),"opencode");return k(J,x)}function N(Q){return typeof Q==="object"&&Q!==null&&!Array.isArray(Q)}async function Xz(){try{let Q=await O(w(),"utf8"),J=[],X=M.parse(Q,J,{allowTrailingComma:!0});if(J.length>0)return{};return N(X)?X:{}}catch{return{}}}var i="openai-auth",s=160,b={forceToTop:!1,order:s,startCollapsed:!1,rememberCollapsed:!0,collapsed:null,pollMs:1500,refreshDebounceMs:200,header:{label:"OPENAI",showVersion:!0},sections:{quota:!0,fallbackAccounts:!0,routing:!0,health:!0,pacing:!0},appearance:{barWidth:10,barFilledChar:"█",barEmptyChar:"░",warnThreshold:50,errorThreshold:80}};function V(Q,J){return typeof Q==="boolean"?Q:J}function B(Q,J,X,Z){if(typeof Q!=="number"||!Number.isFinite(Q))return J;return Math.min(Math.max(Math.round(Q),X),Z)}function d(Q,J,X){if(typeof Q!=="string"||Q.length===0)return J;return Q.slice(0,X)}function h(Q,J){if(typeof Q!=="string")return J;return[...Q][0]??J}function Zz(Q){let J=Q[i];if(!N(J))return structuredClone(b);let X=b,Z=N(J.header)?J.header:{},G=N(J.sections)?J.sections:{},$=N(J.appearance)?J.appearance:{},W=B($.warnThreshold,X.appearance.warnThreshold,0,99),H=Math.max(B($.errorThreshold,X.appearance.errorThreshold,0,100),Math.min(W+1,100));return{forceToTop:V(J.forceToTop,X.forceToTop),order:B(J.order,X.order,-1e4,1e4),startCollapsed:V(J.startCollapsed,X.startCollapsed),rememberCollapsed:V(J.rememberCollapsed,X.rememberCollapsed),collapsed:typeof J.collapsed==="boolean"?J.collapsed:null,pollMs:B(J.pollMs,X.pollMs,500,30000),refreshDebounceMs:B(J.refreshDebounceMs,X.refreshDebounceMs,50,5000),header:{label:d(Z.label,X.header.label,20),showVersion:V(Z.showVersion,X.header.showVersion)},sections:{quota:V(G.quota,X.sections.quota),fallbackAccounts:V(G.fallbackAccounts,X.sections.fallbackAccounts),routing:V(G.routing,X.sections.routing),health:V(G.health,X.sections.health),pacing:V(G.pacing,X.sections.pacing)},appearance:{barWidth:B($.barWidth,X.appearance.barWidth,4,40),barFilledChar:h($.barFilledChar,X.appearance.barFilledChar),barEmptyChar:h($.barEmptyChar,X.appearance.barEmptyChar),warnThreshold:W,errorThreshold:H}}}var n=-1e5;function $z(Q,J,X){let Z=Q[J];if(!N(Z))return X;if(Z.forceToTop===!0)return n+Object.keys(Q).indexOf(J);return B(Z.order,X,-1e4,1e4)}var t=`// Shared preferences for opencode TUI plugins.
2
2
  // One top-level key per plugin (short name). See each plugin's README for
3
3
  // its supported settings. This file is safe to hand-edit; plugins update
4
4
  // individual keys in place and preserve comments.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PackageVersion = "0.4.1";
1
+ export declare const PackageVersion = "0.4.3";
package/dist/ws-pool.d.ts CHANGED
@@ -20,17 +20,8 @@ export interface CreateWebSocketFetchOptions {
20
20
  * shared mutable global.
21
21
  */
22
22
  onQuota?: (s: Record<string, unknown>, accessToken: string, accountId: string | undefined, servedChatgptAccountId: string | undefined) => void;
23
- /**
24
- * Fires when a response.failed frame carries a rate_limit_reached_type —
25
- * mid-stream quota exhaustion the reactive HTTP-status fallback cannot see
26
- * on the WebSocket transport (ws.ts synthesizes status:200 at upgrade,
27
- * before generation runs).
28
- *
29
- * Receives the window label plus the per-request internal quota account key
30
- * captured at send time, same as onQuota, so the signal is attributed to
31
- * the connection's own account rather than a shared mutable global.
32
- */
33
- onRateLimitReached?: (window: string, accountId: string | undefined) => void;
23
+ /** Receives quota exhaustion plus the account identity captured at send time. */
24
+ onRateLimitReached?: (window: string, accountId: string | undefined, resetAt?: number) => void;
34
25
  }
35
26
  interface PoolEntry {
36
27
  socket?: WebSocket;
package/dist/ws.d.ts CHANGED
@@ -30,8 +30,8 @@ export interface StreamResponsesWebSocketOptions {
30
30
  onAbort?: (error: Error) => void;
31
31
  /** Push per-turn quota from a codex.rate_limits in-band frame. */
32
32
  onQuota?: (s: Record<string, unknown>) => void;
33
- /** Called when response.failed carries a rate_limit_reached_type — the only mid-stream quota-exhaustion signal on this transport. */
34
- onRateLimitReached?: (window: string) => void;
33
+ /** Called when the transport reports quota exhaustion for the current connection. */
34
+ onRateLimitReached?: (window: string, resetAt?: number) => void;
35
35
  }
36
36
  export interface WrappedError {
37
37
  status: number;
@@ -41,6 +41,11 @@ export interface WrappedError {
41
41
  export declare function toWebSocketUrl(url: string): string;
42
42
  export declare function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string>;
43
43
  export declare function isAbortError(error: unknown): error is DOMException;
44
+ export declare function isUpgradeFailure(error: unknown): boolean;
44
45
  export declare function connectResponsesWebSocket(options: ConnectResponsesWebSocketOptions): Promise<WebSocket>;
45
46
  export declare function streamResponsesWebSocket(options: StreamResponsesWebSocketOptions): Response;
47
+ export declare function parseRateLimitSignal(value: unknown): {
48
+ window: string;
49
+ resetAt?: number;
50
+ } | undefined;
46
51
  export * as OpenAIWebSocket from './ws';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-openai-auth",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -61,9 +61,9 @@
61
61
  "@opencode-ai/plugin": "*"
62
62
  },
63
63
  "dependencies": {
64
- "@opentui/core": ">=0.4.4",
65
- "@opentui/solid": ">=0.4.4",
66
- "ai": "^7.0.30",
64
+ "@opentui/core": ">=0.4.5",
65
+ "@opentui/solid": ">=0.4.5",
66
+ "ai": "^7.0.34",
67
67
  "jsonc-parser": "^3.3.1",
68
68
  "solid-js": "1.9.12"
69
69
  }
@@ -95,6 +95,22 @@ export async function acquireRefreshFileLock(options: {
95
95
  } catch (error) {
96
96
  const code = (error as NodeJS.ErrnoException).code
97
97
  if (code === 'EEXIST' || code === 'EISDIR') return false
98
+ if (code === 'ENOENT') {
99
+ // Reboots can clear runtime temp directories before the next lock acquisition.
100
+ await mkdir(dirname(lockPath), { recursive: true })
101
+ try {
102
+ await writeFile(
103
+ lockPath,
104
+ `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
105
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' },
106
+ )
107
+ return true
108
+ } catch (retryError) {
109
+ const retryCode = (retryError as NodeJS.ErrnoException).code
110
+ if (retryCode === 'EEXIST' || retryCode === 'EISDIR') return false
111
+ throw retryError
112
+ }
113
+ }
98
114
  throw error
99
115
  }
100
116
  }
@@ -295,6 +295,7 @@ export const ACTIVE_ROUTING_MAX_ENTRIES = 128
295
295
  export type SidebarRoutingAccount = {
296
296
  id: string
297
297
  enabled?: boolean
298
+ killed?: boolean
298
299
  }
299
300
 
300
301
  export function isUsableRoutingEntry(
@@ -308,11 +309,30 @@ export function isUsableRoutingEntry(
308
309
  return (
309
310
  entry.activeId === 'main' ||
310
311
  accounts.some(
311
- (account) => account.enabled !== false && account.id === entry.activeId,
312
+ (account) =>
313
+ account.enabled !== false &&
314
+ account.killed !== true &&
315
+ account.id === entry.activeId,
312
316
  )
313
317
  )
314
318
  }
315
319
 
320
+ export function isQuotaExhausted(
321
+ quota: AccountQuota | null | undefined,
322
+ now = Date.now(),
323
+ ): boolean {
324
+ const primary = quota?.primary
325
+ if (
326
+ typeof primary?.resetsAt !== 'string' ||
327
+ !Number.isFinite(primary.usedPercent) ||
328
+ primary.usedPercent < 100
329
+ ) {
330
+ return false
331
+ }
332
+ const resetsAt = Date.parse(primary.resetsAt)
333
+ return Number.isFinite(resetsAt) && resetsAt > now
334
+ }
335
+
316
336
  export function resolveSessionSidebarRouting(
317
337
  state: SidebarState,
318
338
  sessionId?: string,
@@ -322,13 +342,24 @@ export function resolveSessionSidebarRouting(
322
342
  return { activeId: state.activeId ?? 'main', route: state.route }
323
343
  }
324
344
  const own = sessionId ? state.activeRouting?.[sessionId] : undefined
325
- if (own && isUsableRoutingEntry(own, state.fallbacks, now)) {
345
+ const ownQuota =
346
+ own?.activeId === 'main'
347
+ ? state.main.quota
348
+ : state.fallbacks.find((account) => account.id === own?.activeId)?.quota
349
+ if (
350
+ own &&
351
+ isUsableRoutingEntry(own, state.fallbacks, now) &&
352
+ !isQuotaExhausted(ownQuota, now)
353
+ ) {
326
354
  return { activeId: own.activeId, route: own.route }
327
355
  }
328
356
 
329
- const fallback = state.fallbacks.find(
357
+ const enabledFallbacks = state.fallbacks.filter(
330
358
  (account) => account.enabled && !account.killed,
331
359
  )
360
+ const fallback =
361
+ enabledFallbacks.find((account) => !isQuotaExhausted(account.quota, now)) ??
362
+ enabledFallbacks[0]
332
363
  return {
333
364
  activeId:
334
365
  state.route === 'fallback-first' && fallback ? fallback.id : 'main',
@@ -0,0 +1,309 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
3
+ import { homedir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+
6
+ const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis)
7
+ const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis)
8
+
9
+ export const ACCOUNT_FILE_NAME = 'openai-auth.json'
10
+ export const ACCOUNT_STATE_FILE_NAME = 'openai-auth-state.json'
11
+
12
+ function getConfigDir() {
13
+ if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
14
+ return process.env.OPENCODE_CONFIG_DIR.trim()
15
+ }
16
+ return join(
17
+ process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
18
+ 'opencode',
19
+ )
20
+ }
21
+
22
+ // A concurrent contender renaming the freshly-created eviction-marker directory
23
+ // away surfaces the vanished parent differently per platform: ENOENT on Linux,
24
+ // EINVAL or ENOTDIR on macOS/APFS. All three mean the marker is no longer ours
25
+ // to hold — a lost race the caller should retry, not a fatal lock error.
26
+ export function isLostMarkerRaceError(error: unknown): boolean {
27
+ const code = (error as NodeJS.ErrnoException | null)?.code
28
+ return code === 'ENOENT' || code === 'EINVAL' || code === 'ENOTDIR'
29
+ }
30
+
31
+ export function getAccountStoragePath() {
32
+ return (
33
+ process.env.OPENCODE_OPENAI_AUTH_FILE?.trim() ||
34
+ join(getConfigDir(), ACCOUNT_FILE_NAME)
35
+ )
36
+ }
37
+
38
+ export function getAccountStatePath(configPath = getAccountStoragePath()) {
39
+ const explicit = process.env.OPENCODE_OPENAI_AUTH_STATE_FILE?.trim()
40
+ if (explicit) return explicit
41
+ return configPath.endsWith(ACCOUNT_FILE_NAME)
42
+ ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
43
+ : `${configPath}.state.json`
44
+ }
45
+
46
+ export async function acquireRefreshFileLock(options: {
47
+ name: string
48
+ ttlMs: number
49
+ path?: string
50
+ now?: () => number
51
+ renew?: boolean
52
+ renewIntervalMs?: number
53
+ onStep?: (
54
+ step:
55
+ | 'stale-marker-stat'
56
+ | 'stale-marker-claimed'
57
+ | 'stale-lock-confirmed'
58
+ | 'eviction-marker-acquired',
59
+ ) => void | Promise<void>
60
+ }): Promise<{ release: () => Promise<void> } | null> {
61
+ const accountPath = options.path ?? getAccountStoragePath()
62
+ const lockPath = `${accountPath}.${options.name}.lock`
63
+ const legacyOwnerPath = join(lockPath, 'owner.json')
64
+ const ownerId = randomUUID()
65
+ const now = options.now ?? Date.now
66
+ let renewTimer: ReturnType<typeof setTimeout> | null = null
67
+ let released = false
68
+
69
+ async function readOwner() {
70
+ try {
71
+ return JSON.parse(await readFile(lockPath, 'utf8'))
72
+ } catch (error) {
73
+ const code = (error as NodeJS.ErrnoException).code
74
+ if (code !== 'EISDIR') throw error
75
+ return JSON.parse(await readFile(legacyOwnerPath, 'utf8'))
76
+ }
77
+ }
78
+
79
+ async function writeOwner() {
80
+ await writeFile(
81
+ lockPath,
82
+ `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
83
+ { encoding: 'utf8', mode: 0o600 },
84
+ )
85
+ }
86
+
87
+ async function tryAcquire() {
88
+ try {
89
+ await writeFile(
90
+ lockPath,
91
+ `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
92
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' },
93
+ )
94
+ return true
95
+ } catch (error) {
96
+ const code = (error as NodeJS.ErrnoException).code
97
+ if (code === 'EEXIST' || code === 'EISDIR') return false
98
+ if (code === 'ENOENT') {
99
+ // Reboots can clear runtime temp directories before the next lock acquisition.
100
+ await mkdir(dirname(lockPath), { recursive: true })
101
+ try {
102
+ await writeFile(
103
+ lockPath,
104
+ `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
105
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' },
106
+ )
107
+ return true
108
+ } catch (retryError) {
109
+ const retryCode = (retryError as NodeJS.ErrnoException).code
110
+ if (retryCode === 'EEXIST' || retryCode === 'EISDIR') return false
111
+ throw retryError
112
+ }
113
+ }
114
+ throw error
115
+ }
116
+ }
117
+
118
+ function scheduleRenewal() {
119
+ if (!options.renew || released) return
120
+ const intervalMs =
121
+ options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3))
122
+ renewTimer = setRefreshLockRenewalTimeout(() => {
123
+ void (async () => {
124
+ try {
125
+ const owner = await readOwner()
126
+ const currentNow = now()
127
+ if (
128
+ released ||
129
+ owner?.ownerId !== ownerId ||
130
+ Number(owner?.expiresAt) <= currentNow
131
+ ) {
132
+ return
133
+ }
134
+ await writeOwner()
135
+ scheduleRenewal()
136
+ } catch {
137
+ // If renewal fails, contenders will wait until the last written expiry.
138
+ }
139
+ })()
140
+ }, intervalMs)
141
+ if ('unref' in renewTimer) renewTimer.unref()
142
+ }
143
+
144
+ let acquired = await tryAcquire()
145
+ if (!acquired) {
146
+ // Fencing-token eviction: a directory-based marker (mkdir O_EXCL) serializes
147
+ // destructive removal to one contender at a time. The marker holds an owner
148
+ // file so ownership survives a stale-marker recovery rename: the recovering
149
+ // contender renames the stale directory, then must re-check ownership before
150
+ // acting — preventing the 3rd interleaving where a stale observer renames the
151
+ // FRESH marker the mkdir-winner created.
152
+ const evictPath = `${lockPath}.evicting`
153
+ const evictOwnerPath = join(evictPath, 'owner.json')
154
+ const evictOwnerId = randomUUID()
155
+ const EVICT_TTL = 5_000
156
+ const MAX_STEAL_ATTEMPTS = 8
157
+
158
+ async function backoff() {
159
+ await new Promise((resolve) =>
160
+ setTimeout(resolve, Math.floor(Math.random() * 4)),
161
+ )
162
+ }
163
+
164
+ async function lockIsLive() {
165
+ try {
166
+ const currentOwner = await readOwner()
167
+ return Number(currentOwner?.expiresAt) > now()
168
+ } catch {
169
+ try {
170
+ const current = await stat(lockPath)
171
+ return current.mtimeMs + options.ttlMs > now()
172
+ } catch {
173
+ // Lock doesn't exist — safe to acquire.
174
+ return false
175
+ }
176
+ }
177
+ }
178
+
179
+ // Fail-closed: any read error means we do NOT own the marker.
180
+ async function ownsEvictionMarker() {
181
+ try {
182
+ const owner = JSON.parse(await readFile(evictOwnerPath, 'utf8'))
183
+ return owner?.ownerId === evictOwnerId
184
+ } catch {
185
+ return false
186
+ }
187
+ }
188
+
189
+ async function tryAcquireEvictionMarker() {
190
+ await mkdir(evictPath)
191
+ try {
192
+ await writeFile(
193
+ evictOwnerPath,
194
+ `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`,
195
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' },
196
+ )
197
+ } catch (error) {
198
+ // A competing contender can rename our just-created marker directory
199
+ // away between the mkdir above and this write (the stale-marker steal
200
+ // path below does exactly that). That is a lost race, not a failure, so
201
+ // report it as such and let the caller back off and retry rather than
202
+ // failing the whole lock acquisition.
203
+ if (isLostMarkerRaceError(error)) return false
204
+ await releaseEvictionMarker()
205
+ throw error
206
+ }
207
+ await options.onStep?.('eviction-marker-acquired')
208
+ return true
209
+ }
210
+
211
+ async function releaseEvictionMarker() {
212
+ if (await ownsEvictionMarker()) {
213
+ await rm(evictPath, { recursive: true, force: true }).catch(() => {})
214
+ }
215
+ }
216
+
217
+ for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
218
+ acquired = await tryAcquire()
219
+ if (acquired) break
220
+ if (await lockIsLive()) return null
221
+
222
+ try {
223
+ if (!(await tryAcquireEvictionMarker())) {
224
+ await backoff()
225
+ continue
226
+ }
227
+ } catch (evictError) {
228
+ const code = (evictError as NodeJS.ErrnoException).code
229
+ if (code !== 'EEXIST') throw evictError
230
+
231
+ let evictStat: Awaited<ReturnType<typeof stat>>
232
+ try {
233
+ evictStat = await stat(evictPath)
234
+ } catch (statError) {
235
+ if ((statError as NodeJS.ErrnoException).code === 'ENOENT') {
236
+ await backoff()
237
+ continue
238
+ }
239
+ throw statError
240
+ }
241
+ if (evictStat.mtimeMs + EVICT_TTL > now()) return null
242
+
243
+ await options.onStep?.('stale-marker-stat')
244
+ const claimedPath = `${evictPath}.${randomUUID()}`
245
+ try {
246
+ await rename(evictPath, claimedPath)
247
+ } catch (renameError) {
248
+ if ((renameError as NodeJS.ErrnoException).code === 'ENOENT') {
249
+ await backoff()
250
+ continue
251
+ }
252
+ throw renameError
253
+ }
254
+ await options.onStep?.('stale-marker-claimed')
255
+ await rm(claimedPath, { recursive: true, force: true }).catch(() => {})
256
+ await backoff()
257
+ continue
258
+ }
259
+
260
+ try {
261
+ if (await lockIsLive()) return null
262
+ // Fence check 1: verify we still own the marker before acting on the
263
+ // stale-lock-confirmed decision.
264
+ if (!(await ownsEvictionMarker())) return null
265
+ await options.onStep?.('stale-lock-confirmed')
266
+ // Fence check 2: re-verify ownership after the seam (another contender
267
+ // may have renamed our fresh marker while we were paused here).
268
+ if (!(await ownsEvictionMarker())) return null
269
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {})
270
+ // Fence check 3: re-verify ownership after removing the stale lock.
271
+ if (!(await ownsEvictionMarker())) return null
272
+ acquired = await tryAcquire()
273
+ if (!acquired) return null
274
+ // Fence check 4: re-verify ownership after acquiring the lock. If the
275
+ // marker was stolen between tryAcquire and this check, release the
276
+ // just-acquired lock and return null (fail-closed).
277
+ if (!(await ownsEvictionMarker())) {
278
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {})
279
+ acquired = false
280
+ return null
281
+ }
282
+ break
283
+ } finally {
284
+ await releaseEvictionMarker()
285
+ }
286
+ }
287
+ }
288
+
289
+ if (!acquired) return null
290
+
291
+ scheduleRenewal()
292
+
293
+ return {
294
+ release: async () => {
295
+ released = true
296
+ if (renewTimer) {
297
+ clearRefreshLockRenewalTimeout(renewTimer)
298
+ renewTimer = null
299
+ }
300
+ try {
301
+ const owner = await readOwner()
302
+ if (owner?.ownerId !== ownerId) return
303
+ } catch {
304
+ return
305
+ }
306
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {})
307
+ },
308
+ }
309
+ }
@@ -295,6 +295,7 @@ export const ACTIVE_ROUTING_MAX_ENTRIES = 128
295
295
  export type SidebarRoutingAccount = {
296
296
  id: string
297
297
  enabled?: boolean
298
+ killed?: boolean
298
299
  }
299
300
 
300
301
  export function isUsableRoutingEntry(
@@ -308,11 +309,30 @@ export function isUsableRoutingEntry(
308
309
  return (
309
310
  entry.activeId === 'main' ||
310
311
  accounts.some(
311
- (account) => account.enabled !== false && account.id === entry.activeId,
312
+ (account) =>
313
+ account.enabled !== false &&
314
+ account.killed !== true &&
315
+ account.id === entry.activeId,
312
316
  )
313
317
  )
314
318
  }
315
319
 
320
+ export function isQuotaExhausted(
321
+ quota: AccountQuota | null | undefined,
322
+ now = Date.now(),
323
+ ): boolean {
324
+ const primary = quota?.primary
325
+ if (
326
+ typeof primary?.resetsAt !== 'string' ||
327
+ !Number.isFinite(primary.usedPercent) ||
328
+ primary.usedPercent < 100
329
+ ) {
330
+ return false
331
+ }
332
+ const resetsAt = Date.parse(primary.resetsAt)
333
+ return Number.isFinite(resetsAt) && resetsAt > now
334
+ }
335
+
316
336
  export function resolveSessionSidebarRouting(
317
337
  state: SidebarState,
318
338
  sessionId?: string,
@@ -322,13 +342,24 @@ export function resolveSessionSidebarRouting(
322
342
  return { activeId: state.activeId ?? 'main', route: state.route }
323
343
  }
324
344
  const own = sessionId ? state.activeRouting?.[sessionId] : undefined
325
- if (own && isUsableRoutingEntry(own, state.fallbacks, now)) {
345
+ const ownQuota =
346
+ own?.activeId === 'main'
347
+ ? state.main.quota
348
+ : state.fallbacks.find((account) => account.id === own?.activeId)?.quota
349
+ if (
350
+ own &&
351
+ isUsableRoutingEntry(own, state.fallbacks, now) &&
352
+ !isQuotaExhausted(ownQuota, now)
353
+ ) {
326
354
  return { activeId: own.activeId, route: own.route }
327
355
  }
328
356
 
329
- const fallback = state.fallbacks.find(
357
+ const enabledFallbacks = state.fallbacks.filter(
330
358
  (account) => account.enabled && !account.killed,
331
359
  )
360
+ const fallback =
361
+ enabledFallbacks.find((account) => !isQuotaExhausted(account.quota, now)) ??
362
+ enabledFallbacks[0]
332
363
  return {
333
364
  activeId:
334
365
  state.route === 'fallback-first' && fallback ? fallback.id : 'main',