fix(ci): repair release-root regressions — pack dedup, web-session syntax, uc-image ids (#12423)

Three regressions inherited by every PR rebased onto release/v3.8.51, caught and documented with the exact failing output.

The one that mattered most: src/shared/providers/webSessionCredentials.ts did not parse. The UC merge (#11513) inserted the uc: entry inside maxai.storageKeys and lost the array's closing ], plus the entry's }, leaving `ERROR: Expected "]" but found ":"` at line 351. That module is imported by the provider API routes, bulk-web-session, autoCombo's virtualFactory, keepaliveThreshold and dashboard components, so the break was live on the tip and flooded unrelated catalog tests with transform failures. That was my conflict resolution, not the contributor's code — thank you for catching it and for tracing it to the root commit rather than patching around the symptom.

Also fixed: the duplicate bin/cli/utils/volatileEnvPath.mjs entry in PACK_ARTIFACT_REQUIRED_PATHS (findMissingArtifactPaths reported it twice), and UC image models made prefix-addressable without letting them claim historical bare model ids belonging to other providers.

Reconciled on merge: #12394 landed the busy_timeout/probe work first, so src/lib/db/core.ts takes the tip's side. probeUtils.ts is the union of both rather than either side — this PR's message regex is wider (SQLite also reports "database table is locked", "database schema is locked" and "database is busy"), while #12394 added the driver code/errcode path that keeps a transient lock from being classified as corruption and renaming the database away. Taking either alone would have dropped the other half; this PR's own ENOENT test is what surfaced it.

Verified: 76/76 across uc-image, probe-9541-repro, web-session-contract, pack-artifact-policy, bulk-web-session-import and exclusive-connection-leases, and every changed .ts file parses.

Thanks @backryun.
This commit is contained in:
backryun
2026-09-02 15:57:46 +09:00
committed by GitHub
parent 2c6e6cd13e
commit 752aac65d6
6 changed files with 76 additions and 48 deletions

View File

@@ -276,45 +276,6 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"],
},
// UC (uncensored.com) image generation. Two surfaces served by one handler
// (handleUcImageGeneration picks by credential): PERSONA web (un-metered,
// Clerk JWT -> internal.chatuncensored.ai/v2/image-gen + result-URL polling)
// and uc-direct REST (metered, X-api-key -> api.uncensored.com, OpenAI-shaped).
uc: {
id: "uc",
baseUrl: "https://internal.chatuncensored.ai/v2/image-gen",
authType: "apikey",
authHeader: "bearer",
format: "uc-image",
models: [
{ id: "model-dev", name: "Flux Dev (UC)" },
{ id: "model-pro", name: "Flux Pro (UC)" },
{ id: "model-1.1", name: "Flux Pro 1.1 (UC)" },
{ id: "model-1.2", name: "Wan 2.2 (UC)" },
{ id: "seedream-v4.5", name: "Seedream v4.5 (UC)" },
{ id: "seedream-v5", name: "Seedream v5 (UC)" },
{ id: "flux-2", name: "FLUX.2 (UC)" },
{ id: "flux-2-pro", name: "FLUX.2 Pro (UC)" },
{ id: "lustify-v7", name: "Lustify v7 (UC)" },
{ id: "nano-banana", name: "Nano Banana (UC)" },
{ id: "nano-banana-2", name: "Nano Banana 2 (UC)" },
{ id: "nano-banana-pro", name: "Nano Banana Pro (UC)" },
{ id: "nano-banana-ultra", name: "Nano Banana Ultra (UC)" },
{ id: "gpt-image", name: "GPT Image (UC)" },
{ id: "gpt-image-2", name: "GPT Image 2 (UC)" },
{ id: "realism", name: "Realism (UC)" },
{ id: "realism-2", name: "Realism 2 (UC)" },
{ id: "z-image-turbo", name: "Z-Image Turbo (UC)" },
{ id: "prefect-pony-xl", name: "Prefect Pony XL (UC)" },
{ id: "wan-2.6", name: "Wan 2.6 (UC)" },
{ id: "wan-2.7-text-to-image", name: "Wan 2.7 Text-to-Image (UC)" },
{ id: "wan-2.7-text-to-image-pro", name: "Wan 2.7 Text-to-Image Pro (UC)" },
],
// Persona web derives imageWidth/imageHeight from an aspect ratio; uc-direct
// passes any OpenAI-style size through. These are the aspect buckets.
supportedSizes: ["1024x1024", "1024x576", "576x1024", "1024x768", "768x1024"],
},
xai: {
id: "xai",
baseUrl: "https://api.x.ai/v1/images/generations",
@@ -894,6 +855,44 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1024x1024", "2048x2048"],
},
aihorde: AI_HORDE_IMAGE_PROVIDER,
// Keep UC after every existing image provider because parseImageModel() resolves
// bare duplicate ids by first match. Explicit `uc/` routes remain available while
// historical owners retain bare ids such as nano-banana and z-image-turbo.
uc: {
id: "uc",
baseUrl: "https://internal.chatuncensored.ai/v2/image-gen",
authType: "apikey",
authHeader: "bearer",
format: "uc-image",
models: [
{ id: "model-dev", name: "Flux Dev (UC)" },
{ id: "model-pro", name: "Flux Pro (UC)" },
{ id: "model-1.1", name: "Flux Pro 1.1 (UC)" },
{ id: "model-1.2", name: "Wan 2.2 (UC)" },
{ id: "seedream-v4.5", name: "Seedream v4.5 (UC)" },
{ id: "seedream-v5", name: "Seedream v5 (UC)" },
{ id: "flux-2", name: "FLUX.2 (UC)" },
{ id: "flux-2-pro", name: "FLUX.2 Pro (UC)" },
{ id: "lustify-v7", name: "Lustify v7 (UC)" },
{ id: "nano-banana", name: "Nano Banana (UC)" },
{ id: "nano-banana-2", name: "Nano Banana 2 (UC)" },
{ id: "nano-banana-pro", name: "Nano Banana Pro (UC)" },
{ id: "nano-banana-ultra", name: "Nano Banana Ultra (UC)" },
{ id: "gpt-image", name: "GPT Image (UC)" },
{ id: "gpt-image-2", name: "GPT Image 2 (UC)" },
{ id: "realism", name: "Realism (UC)" },
{ id: "realism-2", name: "Realism 2 (UC)" },
{ id: "z-image-turbo", name: "Z-Image Turbo (UC)" },
{ id: "prefect-pony-xl", name: "Prefect Pony XL (UC)" },
{ id: "wan-2.6", name: "Wan 2.6 (UC)" },
{ id: "wan-2.7-text-to-image", name: "Wan 2.7 Text-to-Image (UC)" },
{ id: "wan-2.7-text-to-image-pro", name: "Wan 2.7 Text-to-Image Pro (UC)" },
],
// Persona web derives imageWidth/imageHeight from an aspect ratio; uc-direct
// passes any OpenAI-style size through. These are the aspect buckets.
supportedSizes: ["1024x1024", "1024x576", "576x1024", "1024x768", "768x1024"],
},
};
/**

View File

@@ -216,12 +216,6 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
"bin/mcpStdioConsoleGuard.mjs",
"bin/nodeRuntimeSupport.mjs",
"bin/omniroute.mjs",
// #11437: bin/omniroute.mjs imports ./cli/utils/volatileEnvPath.mjs at startup
// (describeVolatileEnvWarning — flags a .env living inside the installed package).
// bin/cli/ is only an allowlist PREFIX, so its absence would never fail the
// unexpected-paths check; list it REQUIRED so a regression is loud (#7065 class,
// enforced by tests/unit/pack-artifact-entrypoint-closures.test.ts).
"bin/cli/utils/volatileEnvPath.mjs",
// #7808: aliasResolver + its hook file. bin/omniroute.mjs imports
// bin/aliasResolver.mjs at startup, which in turn registers
// bin/aliasResolverHook.mjs as the ESM loader. Both must ship in the tarball

View File

@@ -22,7 +22,14 @@ import path from "node:path";
*/
export function isTransientProbeError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
if (/SQLITE_BUSY|SQLITE_PROTOCOL|SQLITE_IOERR|ENOENT|database is locked/i.test(message)) {
// #12423 widened the message side: SQLite also reports "database table is
// locked", "database schema is locked" and "database is busy" for the same
// transient contention that "database is locked" covers.
if (
/SQLITE_BUSY|SQLITE_PROTOCOL|SQLITE_IOERR|ENOENT|database(?: table| schema)? is (?:locked|busy)/i.test(
message
)
) {
return true;
}
// The real drivers do not put the result-code name in the message: both

View File

@@ -348,6 +348,8 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
"maxaiDeviceId",
"userId",
"maxaiUserId",
],
},
uc: {
// UC (uncensored.com) persona: auth is the durable Clerk `__client` cookie
// (a JWT with no exp) plus the session id + user id, all stored in

View File

@@ -50,6 +50,11 @@ test("FIX-GREEN: isTransientProbeError does NOT classify fatal errors", () => {
test("FIX-GREEN: isTransientProbeError classifies BUSY/PROTOCOL/IOERR/ENOENT", () => {
const transientPatterns = [
"SQLITE_BUSY: database is locked",
// better-sqlite3 can omit the symbolic SQLite error code entirely.
"database is locked",
"database table is locked",
"database schema is locked: main",
"database is busy",
"SQLITE_PROTOCOL: locking protocol",
"SQLITE_IOERR: disk I/O error",
"ENOENT: no such file or directory, open '/tmp/db.sqlite'",

View File

@@ -8,7 +8,7 @@ import {
UC_PERSONA_IMAGE_URL,
UC_DIRECT_IMAGE_URL,
} from "../../open-sse/handlers/imageGeneration/providers/ucImage.ts";
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
import { IMAGE_PROVIDERS, parseImageModel } from "../../open-sse/config/imageRegistry.ts";
// A valid PERSONA credential (durable Clerk cookie + sid + uid in psd). No API
// key, so the handler takes the persona web path (mint -> POST -> poll).
@@ -51,6 +51,25 @@ test("uc is registered in IMAGE_PROVIDERS with the uc-image format + 22 models",
assert.equal((entry.models ?? []).length, 22);
});
test("uc image models require an explicit prefix when an existing provider owns the bare id", () => {
assert.deepEqual(parseImageModel("uc/nano-banana"), {
provider: "uc",
model: "nano-banana",
});
assert.deepEqual(parseImageModel("uc/z-image-turbo"), {
provider: "uc",
model: "z-image-turbo",
});
assert.deepEqual(parseImageModel("nano-banana"), {
provider: "adobe-firefly",
model: "nano-banana",
});
assert.deepEqual(parseImageModel("z-image-turbo"), {
provider: "nanogpt",
model: "z-image-turbo",
});
});
// --- Pure helpers --------------------------------------------------------
test("resolveUcImageModel strips uc/ and uc-direct/ prefixes", () => {
@@ -226,7 +245,9 @@ test("handleUcImageGeneration (persona) 401s (retryable) when the credential is
test("handleUcImageGeneration (persona) times out with 504 when the result never readies", async () => {
const resultUrl = "https://gen.moveinwater.com/img_never.png";
const fetchImpl = personaFetch({
pendingPolls: 1000, // never becomes ready within the window
// The injected no-op sleep can execute more than 1,000 polls inside 5 ms on
// fast runners, so use an unbounded pending count to make the timeout deterministic.
pendingPolls: Number.POSITIVE_INFINITY,
resultUrl,
jwt: fakeJwt("uid", FUTURE_EXP),
});