teamclaude-cloud 1.8.1 → 1.9.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/package.json +1 -1
- package/src/account-manager.js +51 -1
- package/src/cloud.js +181 -7
- package/src/index.js +961 -117
- package/src/server.js +84 -13
- package/src/tui.js +7 -0
package/src/server.js
CHANGED
|
@@ -484,7 +484,7 @@ export function createProxyServer(accountManager, config, hooks = {}) {
|
|
|
484
484
|
// tried429/tried5xx/authRetried hold account OBJECTS (not indexes), and
|
|
485
485
|
// `held` is the acquired account OBJECT — both stable across a concurrent
|
|
486
486
|
// removeAccount() re-index, so a release/exclude can't target the wrong account.
|
|
487
|
-
const ctx = { account: null, status: null, authRetried: new Set(), tried429: new Set(), tried5xx: new Set(), overloadRetries: 0, held: null, queueTimeoutMs, abortSignal: null, affinityKey: sessionAffinity ? req.socket : null, sawModelWeekly: false };
|
|
487
|
+
const ctx = { account: null, status: null, authRetried: new Set(), tried429: new Set(), tried5xx: new Set(), triedError: new Set(), overloadRetries: 0, held: null, queueTimeoutMs, abortSignal: null, affinityKey: sessionAffinity ? req.socket : null, sawModelWeekly: false };
|
|
488
488
|
try {
|
|
489
489
|
// Buffer request body (needed for retry on 429), bounded by maxBodyBytes.
|
|
490
490
|
const bodyChunks = [];
|
|
@@ -776,8 +776,8 @@ async function forwardRequest(req, res, body, accountManager, upstream, retryCou
|
|
|
776
776
|
// Select account. On a failover retry (a prior account 429'd / 5xx'd for this
|
|
777
777
|
// request) ctx.tried* is non-empty → pick a different account, skipping the
|
|
778
778
|
// ones already tried.
|
|
779
|
-
const excludeForSelect = (ctx.tried429.size || ctx.tried5xx.size)
|
|
780
|
-
? new Set([...ctx.tried429, ...ctx.tried5xx])
|
|
779
|
+
const excludeForSelect = (ctx.tried429.size || ctx.tried5xx.size || ctx.triedError.size)
|
|
780
|
+
? new Set([...ctx.tried429, ...ctx.tried5xx, ...ctx.triedError])
|
|
781
781
|
: null;
|
|
782
782
|
|
|
783
783
|
// Reserve a per-account concurrency slot. On a 401 same-account refresh-retry
|
|
@@ -805,19 +805,28 @@ async function forwardRequest(req, res, body, accountManager, upstream, retryCou
|
|
|
805
805
|
|
|
806
806
|
if (!account) {
|
|
807
807
|
ctx.account = '(none available)';
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
808
|
+
// Every account is in error state. Classify by the FAILURE CLASS recorded when each errored:
|
|
809
|
+
// • ALL transient (cloud refresh 5xx/network — the recovery timer heals them) → retryable 503,
|
|
810
|
+
// not a 401 (a 401 "re-login required" would trigger needless client re-auth/teardown for a
|
|
811
|
+
// temporary failure the client can't fix — the proxy's accounts).
|
|
812
|
+
// • ANY terminal failure (a 4xx invalid_grant/revoked key, or a non-cloud local auth failure
|
|
813
|
+
// the recovery timer never touches) → 401 so it isn't silently retried forever.
|
|
811
814
|
const accts = accountManager.accounts;
|
|
812
815
|
if (accts.length > 0 && accts.every(a => a.status === 'error')) {
|
|
816
|
+
if (accts.every(a => a.errorTransient === true)) {
|
|
817
|
+
ctx.status = 503;
|
|
818
|
+
res.writeHead(503, { 'Content-Type': 'application/json', 'retry-after': '30' });
|
|
819
|
+
res.end(JSON.stringify({
|
|
820
|
+
type: 'error',
|
|
821
|
+
error: { type: 'overloaded_error', message: `All ${accts.length} cloud-managed accounts are temporarily unavailable (token refresh failing); recovering — retry shortly.` },
|
|
822
|
+
}));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
813
825
|
ctx.status = 401;
|
|
814
826
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
815
827
|
res.end(JSON.stringify({
|
|
816
828
|
type: 'error',
|
|
817
|
-
error: {
|
|
818
|
-
type: 'authentication_error',
|
|
819
|
-
message: `All ${accts.length} accounts failed authentication. Re-login required.`,
|
|
820
|
-
},
|
|
829
|
+
error: { type: 'authentication_error', message: `All ${accts.length} accounts failed authentication. Re-login required.` },
|
|
821
830
|
}));
|
|
822
831
|
return;
|
|
823
832
|
}
|
|
@@ -871,9 +880,71 @@ async function forwardRequest(req, res, body, accountManager, upstream, retryCou
|
|
|
871
880
|
return;
|
|
872
881
|
}
|
|
873
882
|
|
|
874
|
-
if (account.status === 'error'
|
|
875
|
-
releaseHeld(); //
|
|
876
|
-
|
|
883
|
+
if (account.status === 'error') {
|
|
884
|
+
releaseHeld(); // this account can't serve — fail over, or give up locally
|
|
885
|
+
if (res.destroyed) return; // client gone — outer finally frees the slot
|
|
886
|
+
// Fail over while ANY untried account is still usable — do NOT cap on the shared
|
|
887
|
+
// `retryCount` budget here: 401 same-account refresh-retries and 429/5xx failovers may
|
|
888
|
+
// have already elevated it, which would wrongly 429 the client while a HEALTHY account
|
|
889
|
+
// (e.g. the user's 4th of 11) sits untried. Record this errored account so `anyUsable`
|
|
890
|
+
// shrinks each pass (error accounts are already excluded from selection by status too),
|
|
891
|
+
// guaranteeing termination once no usable account remains.
|
|
892
|
+
ctx.triedError.add(account);
|
|
893
|
+
// Termination is guaranteed by the EXCLUSION SET, not anyUsable's internal availability
|
|
894
|
+
// logic: triedError only grows and holds distinct account OBJECTS, so once every account
|
|
895
|
+
// is in excludeTried, `!excludeTried.has(a)` is false for all → anyUsable returns false
|
|
896
|
+
// regardless of _isAvailable/_hasCapacity. So a future change to anyUsable can't create an
|
|
897
|
+
// unbounded loop (bounded by accounts.length distinct objects).
|
|
898
|
+
const excludeTried = new Set([...ctx.tried429, ...ctx.tried5xx, ...ctx.triedError]);
|
|
899
|
+
// Accept anyCapped too (like the 429/5xx failover paths): a healthy untried account at
|
|
900
|
+
// its concurrency cap will free shortly, so fail over and let acquireAccount's bounded
|
|
901
|
+
// overflow queue wait for it rather than immediately 429'ing under load. Still bounded —
|
|
902
|
+
// a capped account either frees and serves, errors (→ triedError), or the queue times out.
|
|
903
|
+
if (accountManager.anyUsable(excludeTried) || accountManager.anyCapped(excludeTried)) {
|
|
904
|
+
return forwardRequest(req, res, body, accountManager, upstream, retryCount + 1, hooks, reqId, ctx, logDir);
|
|
905
|
+
}
|
|
906
|
+
// No untried usable account remains — the selected account's token is dead (e.g. a
|
|
907
|
+
// cloud-managed account whose central refresh failed; single-authority forbids a local
|
|
908
|
+
// rotation, so ensureTokenFresh left it status='error'). Do NOT forward its EXPIRED
|
|
909
|
+
// bearer token (that returns a confusing upstream 401 and defeats the immediate-
|
|
910
|
+
// unavailability contract). Classify the terminal EXACTLY like the no-account path above so
|
|
911
|
+
// identical "all credentials failed" states behave identically to the client: if EVERY
|
|
912
|
+
// account is in auth-error state it's an authentication problem (re-login) → 401; otherwise
|
|
913
|
+
// it's transient unavailability → local 429 so the client backs off while the recovery timer
|
|
914
|
+
// re-probes the cloud and heals the account.
|
|
915
|
+
if (!res.headersSent) {
|
|
916
|
+
const accts = accountManager.accounts;
|
|
917
|
+
if (accts.length > 0 && accts.every(a => a.status === 'error')) {
|
|
918
|
+
// Same classification as the no-account terminal above (consistent across both
|
|
919
|
+
// "all credentials failed" paths): all-transient → healable → retryable 503; any
|
|
920
|
+
// terminal failure (4xx invalid_grant / non-cloud auth) → 401.
|
|
921
|
+
if (accts.every(a => a.errorTransient === true)) {
|
|
922
|
+
ctx.status = 503;
|
|
923
|
+
res.writeHead(503, { 'Content-Type': 'application/json', 'retry-after': '30' });
|
|
924
|
+
res.end(JSON.stringify({
|
|
925
|
+
type: 'error',
|
|
926
|
+
error: { type: 'overloaded_error', message: `All ${accts.length} cloud-managed accounts are temporarily unavailable (token refresh failing); recovering — retry shortly.` },
|
|
927
|
+
}));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
ctx.status = 401;
|
|
931
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
932
|
+
res.end(JSON.stringify({
|
|
933
|
+
type: 'error',
|
|
934
|
+
error: { type: 'authentication_error', message: `All ${accts.length} accounts failed authentication. Re-login required.` },
|
|
935
|
+
}));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
ctx.status = 429;
|
|
939
|
+
res.writeHead(429, { 'Content-Type': 'application/json', 'retry-after': '10' });
|
|
940
|
+
res.end(JSON.stringify({
|
|
941
|
+
type: 'error',
|
|
942
|
+
error: { type: 'rate_limit_error', message: 'No account with a valid token is available right now; please retry shortly.' },
|
|
943
|
+
}));
|
|
944
|
+
} else {
|
|
945
|
+
ctx.status = ctx.status || 429;
|
|
946
|
+
}
|
|
947
|
+
return;
|
|
877
948
|
}
|
|
878
949
|
|
|
879
950
|
// Build upstream request headers
|
package/src/tui.js
CHANGED
|
@@ -479,6 +479,13 @@ export class TUI {
|
|
|
479
479
|
const prev = this.config.accounts[idx];
|
|
480
480
|
if (prev.enabled !== undefined) entry.enabled = prev.enabled;
|
|
481
481
|
if (prev.priority !== undefined) entry.priority = prev.priority;
|
|
482
|
+
// CRITICAL: preserve cloud provenance/display too — a re-import replaces the TOKEN, not
|
|
483
|
+
// the account's cloud-sharing status. Dropping cloudSources/cloudPushed would make a
|
|
484
|
+
// cloud-managed (shared) account look LOCAL → it would locally rotate its now-shared
|
|
485
|
+
// refresh token and poison the chain for every other PC (single-authority invariant).
|
|
486
|
+
if (Array.isArray(prev.cloudSources) && prev.cloudSources.length) entry.cloudSources = prev.cloudSources;
|
|
487
|
+
if (prev.cloudPushed) entry.cloudPushed = prev.cloudPushed;
|
|
488
|
+
if (prev.cloudPrivate) entry.cloudPrivate = prev.cloudPrivate;
|
|
482
489
|
this.config.accounts[idx] = entry;
|
|
483
490
|
// Update the running account manager entry, matched by IDENTITY (the
|
|
484
491
|
// previous entry's UUID first, then name) — NOT the config index `idx`,
|