From 48a66864d6693ce04899b14ead8da77207b23c93 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sat, 4 Jul 2026 23:55:31 +0100 Subject: [PATCH 1/9] fix(run-ops): DNS-safe, sortable base32hex run id (replace base62 KSUID) The run-ops split minted NEW-store run ids as 27-char base62 KSUIDs. The supervisor puts the run id in the k8s pod name (runner-), and pod names must be DNS-1123 labels (lowercase [a-z0-9-]), so uppercase base62 ids made k8s reject the pod (422) and those runs never launched. .toLowerCase() can't fix it (base62 folds distinct symbols, colliding ids and destroying sort). Change the encoding, not the structure: mint a 26-char lowercase base32hex id (24-char core = 6-byte ms timestamp + 9 CSPRNG bytes, + region char + version char "1"). Lexicographically time-sortable at ms resolution, DNS-safe from birth, hyphen-free (so firekeeper's runner--attempt-N round-trip is unchanged). The residency discriminator switches from id length to the fixed version char, so the format can evolve without a hard migration. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/olive-bees-repeat.md | 5 + .../idempotencyResidency.server.test.ts | 4 +- .../services/triggerFailedTask.server.ts | 4 +- .../runEngine/services/triggerTask.server.ts | 17 +- apps/webapp/app/utils/friendlyId.test.ts | 19 +- apps/webapp/app/utils/friendlyId.ts | 18 +- .../runOpsMigration/crossSeamGuard.server.ts | 1 - .../mintBatchFriendlyId.server.test.ts | 10 +- .../mintBatchFriendlyId.server.ts | 4 +- .../readThrough.server.test.ts | 4 +- .../resolveInheritedMintKind.server.test.ts | 2 +- apps/webapp/app/v3/runStore.server.test.ts | 20 +- .../app/v3/services/batchTriggerV3.server.ts | 13 +- ...lkActionV2.batchReadThrough.server.test.ts | 4 +- .../services/executeTasksWaitingForDeploy.ts | 6 +- .../test/SpanPresenter.readthrough.test.ts | 31 +-- .../test/api.v1.waitpoints.tokens.test.ts | 4 +- ...apiBatchResultsPresenter.readroute.test.ts | 4 +- ...iBatchResultsPresenter.readthrough.test.ts | 4 +- .../apiRetrieveRunPresenter.readroute.test.ts | 22 +- .../apiRunResultPresenter.readthrough.test.ts | 2 +- .../apiWaitpointPresenter.readthrough.test.ts | 12 +- ...batchTriggerV3ResidencyInheritance.test.ts | 10 +- .../test/batchTriggerV3StoreRouting.test.ts | 6 +- .../test/bulkActionV2ReadRouting.test.ts | 4 +- .../cancelDevSessionRunsStoreRouting.test.ts | 10 +- apps/webapp/test/crossSeamGuard.proof.test.ts | 4 +- .../test/engine/triggerFailedTask.test.ts | 8 +- apps/webapp/test/engine/triggerTask.test.ts | 8 +- .../test/idempotencyDedupResidency.test.ts | 4 +- ...empotencyKeyConcernLegacyAuthority.test.ts | 4 +- .../performTaskRunAlertsStoreRouting.test.ts | 8 +- .../realtime/runReaderReadThrough.test.ts | 6 +- ...resetIdempotencyKeyLegacyAuthority.test.ts | 4 +- ...ointThroughReadThrough.readthrough.test.ts | 22 +- apps/webapp/test/runOpsCrossSeamGuard.test.ts | 18 +- apps/webapp/test/runOpsMintCutover.test.ts | 10 +- .../webapp/test/runPresenterReadRoute.test.ts | 10 +- .../test/runsRepository.readthrough.test.ts | 4 +- apps/webapp/test/sessions.readthrough.test.ts | 6 +- .../updateMetadataStoreRoutingHetero.test.ts | 22 +- .../systems/executionSnapshotSystem.test.ts | 4 +- .../engine/systems/waitpointSystem.test.ts | 8 +- .../engine/tests/blockEdgeResidency.test.ts | 6 +- .../completeWaitpointReadResidency.test.ts | 6 +- .../tests/datetimeWaitpointColocation.test.ts | 10 +- .../src/engine/tests/lifecycleRouter.test.ts | 8 +- .../engine/tests/triggerCreateRouting.test.ts | 4 +- ...tgresRunStore.batchProbeReadClient.test.ts | 22 +- ...stgresRunStore.controlPlaneAlertFk.test.ts | 16 +- .../PostgresRunStore.dedicatedRepro.test.ts | 64 +++--- .../PostgresRunStore.writeAtomicity.test.ts | 6 +- .../src/batchCompletionResidency.test.ts | 14 +- .../runOpsStore.flipWindowDuplicate.test.ts | 3 +- ...runOpsStore.forWaitpointCompletion.test.ts | 2 +- .../src/runOpsStore.idempotencyDedup.test.ts | 6 +- .../src/runOpsStore.mixedResidency.test.ts | 4 +- .../src/runOpsStore.readAfterWrite.test.ts | 4 +- .../src/runOpsStore.snapshots.test.ts | 10 +- .../run-store/src/runOpsStore.test.ts | 82 +++---- .../run-store/src/runOpsStore.ts | 5 +- .../src/runOpsStore.waitpoints.test.ts | 16 +- .../core/src/v3/isomorphic/friendlyId.test.ts | 193 +++++++++++----- packages/core/src/v3/isomorphic/friendlyId.ts | 212 ++++++++++-------- .../src/v3/isomorphic/runOpsResidency.test.ts | 57 +++-- .../core/src/v3/isomorphic/runOpsResidency.ts | 46 ++-- 66 files changed, 662 insertions(+), 524 deletions(-) create mode 100644 .changeset/olive-bees-repeat.md diff --git a/.changeset/olive-bees-repeat.md b/.changeset/olive-bees-repeat.md new file mode 100644 index 00000000000..6e3e3572780 --- /dev/null +++ b/.changeset/olive-bees-repeat.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/core": patch +--- + +Change the run-ops run id format from a 27-char base62 KSUID to a 26-char lowercase base32hex id (ms timestamp + CSPRNG core, region char, version char) so run ids are DNS-1123 safe for Kubernetes pod names while staying lexicographically time-sortable diff --git a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts index ee128224d10..72bcd7d9825 100644 --- a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts +++ b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts @@ -18,7 +18,7 @@ function makeDeps(over: Partial): ResolveIdempoten legacyClient: LEGACY_CLIENT, resolveMintKind: async () => "ksuid", classify: (id) => { - if (id.length === 27) return "NEW"; + if (id.length === 26 && id[25] === "1") return "NEW"; if (id.length === 25) return "LEGACY"; throw new Error(`unclassifiable: ${id.length}`); }, @@ -55,7 +55,7 @@ describe("resolveIdempotencyDedupClient", () => { }); it("routes a child to the NEW client when the ksuid parent is NEW-resident", async () => { - const ksuidParent = RunId.toFriendlyId("a".repeat(27)); + const ksuidParent = RunId.toFriendlyId("a".repeat(24) + "01"); const client = await resolveIdempotencyDedupClient( { environmentForMint: env, parentRunFriendlyId: ksuidParent }, makeDeps({ resolveMintKind: async () => "cuid" }) // mint flag must NOT win for a child diff --git a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts index 811cefd3501..0fed5c86d21 100644 --- a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts @@ -1,6 +1,6 @@ import type { RunEngine } from "@internal/run-engine"; import { TaskRunErrorCodes, type TaskRunError } from "@trigger.dev/core/v3"; -import { RunId, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { RunId, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction, RuntimeEnvironmentType, @@ -103,7 +103,7 @@ export class TriggerFailedTaskService { }); return mintKind === "ksuid" - ? RunId.toFriendlyId(generateKsuidId()) + ? RunId.toFriendlyId(generateRunOpsId()) : RunId.generate().friendlyId; } diff --git a/apps/webapp/app/runEngine/services/triggerTask.server.ts b/apps/webapp/app/runEngine/services/triggerTask.server.ts index a86cb9e0eda..e5586b076f4 100644 --- a/apps/webapp/app/runEngine/services/triggerTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerTask.server.ts @@ -14,7 +14,7 @@ import { TriggerTraceContext, } from "@trigger.dev/core/v3"; import { - generateKsuidId, + generateRunOpsId, parseTraceparent, RunId, serializeTraceparent, @@ -136,9 +136,14 @@ export class RunEngineTriggerTaskService { // - CHILD run (has a parent): inherit the parent's residency by id-shape, so a // parent and child never split across stores (ksuid parent → ksuid child, // cuid parent → cuid child). + // `region` is the caller-requested region (body.options.region). The id is + // minted before the worker queue is resolved (the idempotency concern needs + // the friendlyId first), so the stamped region char reflects the requested + // region — or the default char when the run targets the default region. private async mintRunFriendlyId( environment: AuthenticatedEnvironment, - parentRunFriendlyId?: string + parentRunFriendlyId?: string, + region?: string ): Promise { const mintKind = parentRunFriendlyId ? resolveInheritedMintKind(parentRunFriendlyId) @@ -149,7 +154,7 @@ export class RunEngineTriggerTaskService { }); return mintKind === "ksuid" - ? RunId.toFriendlyId(generateKsuidId()) + ? RunId.toFriendlyId(generateRunOpsId(region)) : RunId.generate().friendlyId; } @@ -183,7 +188,11 @@ export class RunEngineTriggerTaskService { // parent is present, else the environment's setting. const runFriendlyId = options?.runFriendlyId ?? - (await this.mintRunFriendlyId(environment, body.options?.parentRunId)); + (await this.mintRunFriendlyId( + environment, + body.options?.parentRunId, + body.options?.region + )); const triggerRequest = { taskId, friendlyId: runFriendlyId, diff --git a/apps/webapp/app/utils/friendlyId.test.ts b/apps/webapp/app/utils/friendlyId.test.ts index 4b2854f51c2..e6c902c96b3 100644 --- a/apps/webapp/app/utils/friendlyId.test.ts +++ b/apps/webapp/app/utils/friendlyId.test.ts @@ -2,30 +2,31 @@ import { describe, expect, it } from "vitest"; import { BatchId, generateFriendlyId, - generateKsuidId, + generateRunOpsId, RunId, } from "@trigger.dev/core/v3/isomorphic"; import { isValidFriendlyId, makeFriendlyIdValidator } from "./friendlyId"; describe("isValidFriendlyId", () => { it("accepts every id generation the real generators produce", () => { - // nanoid (legacy V1), cuid (run-engine), ksuid (run-ops split) + // nanoid (legacy V1), cuid (run-engine), run-ops v1 (run-ops split) expect(isValidFriendlyId(generateFriendlyId("run"), "run")).toBe(true); expect(isValidFriendlyId(RunId.generate().friendlyId, "run")).toBe(true); - expect(isValidFriendlyId(RunId.toFriendlyId(generateKsuidId()), "run")).toBe(true); + expect(isValidFriendlyId(RunId.toFriendlyId(generateRunOpsId()), "run")).toBe(true); expect(isValidFriendlyId(generateFriendlyId("batch"), "batch")).toBe(true); expect(isValidFriendlyId(BatchId.generate().friendlyId, "batch")).toBe(true); - expect(isValidFriendlyId(BatchId.toFriendlyId(generateKsuidId()), "batch")).toBe(true); + expect(isValidFriendlyId(BatchId.toFriendlyId(generateRunOpsId()), "batch")).toBe(true); }); - it("accepts each valid body length (21 nanoid, 25 cuid, 27 ksuid)", () => { + it("accepts each valid body length (21 nanoid, 25 cuid, 26 run-ops v1, 27 legacy base62)", () => { expect(isValidFriendlyId("run_" + "a".repeat(21), "run")).toBe(true); expect(isValidFriendlyId("run_" + "a".repeat(25), "run")).toBe(true); + expect(isValidFriendlyId("run_" + "a".repeat(26), "run")).toBe(true); expect(isValidFriendlyId("run_" + "a".repeat(27), "run")).toBe(true); }); - it("accepts mixed-case (uppercase) ksuid bodies", () => { + it("accepts mixed-case (uppercase) legacy base62 bodies", () => { expect(isValidFriendlyId("run_2ABCdefGHI0123456789jklMN", "run")).toBe(true); }); @@ -39,7 +40,7 @@ describe("isValidFriendlyId", () => { }); it("rejects body lengths that match no generator", () => { - for (const len of [0, 20, 22, 24, 26, 28]) { + for (const len of [0, 20, 22, 24, 28]) { expect(isValidFriendlyId("run_" + "a".repeat(len), "run")).toBe(false); } }); @@ -64,8 +65,8 @@ describe("makeFriendlyIdValidator", () => { it("returns undefined for a valid id of any generation", () => { expect(validateRunId(generateFriendlyId("run"))).toBeUndefined(); expect(validateRunId(RunId.generate().friendlyId)).toBeUndefined(); - expect(validateRunId(RunId.toFriendlyId(generateKsuidId()))).toBeUndefined(); - expect(validateBatchId(BatchId.toFriendlyId(generateKsuidId()))).toBeUndefined(); + expect(validateRunId(RunId.toFriendlyId(generateRunOpsId()))).toBeUndefined(); + expect(validateBatchId(BatchId.toFriendlyId(generateRunOpsId()))).toBeUndefined(); }); it("reports a wrong prefix distinctly from a wrong shape", () => { diff --git a/apps/webapp/app/utils/friendlyId.ts b/apps/webapp/app/utils/friendlyId.ts index bc26ef19b4b..2500d685fa5 100644 --- a/apps/webapp/app/utils/friendlyId.ts +++ b/apps/webapp/app/utils/friendlyId.ts @@ -1,22 +1,26 @@ -import { CUID_LENGTH, KSUID_LENGTH } from "@trigger.dev/core/v3/isomorphic"; +import { CUID_LENGTH, RUN_OPS_ID_LENGTH } from "@trigger.dev/core/v3/isomorphic"; -// The body after `_` is a base62 id; three generator lengths remain -// valid in existing data and must all be accepted: 21 (nanoid), 25 (cuid), -// 27 (ksuid). cuid/ksuid come from core so this tracks any future change. +// The body after `_` is an alphanumeric id; four generator lengths +// remain valid in existing data and must all be accepted: 21 (nanoid), +// 25 (cuid), 26 (run-ops v1 base32hex), 27 (pre-cutover base62, kept so old +// ids still pass filter validation). cuid/run-ops come from core so this +// tracks any future change. const NANOID_BODY_LENGTH = 21; +const LEGACY_BASE62_BODY_LENGTH = 27; const VALID_BODY_LENGTHS: ReadonlySet = new Set([ NANOID_BODY_LENGTH, CUID_LENGTH, - KSUID_LENGTH, + RUN_OPS_ID_LENGTH, + LEGACY_BASE62_BODY_LENGTH, ]); -const BASE62 = /^[0-9A-Za-z]+$/; +const ALPHANUMERIC = /^[0-9A-Za-z]+$/; export function isValidFriendlyId(value: string, prefix: string): boolean { const marker = `${prefix}_`; if (!value.startsWith(marker)) return false; const body = value.slice(marker.length); - return VALID_BODY_LENGTHS.has(body.length) && BASE62.test(body); + return VALID_BODY_LENGTHS.has(body.length) && ALPHANUMERIC.test(body); } export function makeFriendlyIdValidator(prefix: string, label: string) { diff --git a/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts b/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts index 791a101e9e3..ee836f57eb3 100644 --- a/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts @@ -56,7 +56,6 @@ export function selectStoreForWaitpoint( const classify = deps?.classify ?? ownerEngine; - // Loud on ambiguity: classify throws UnclassifiableRunId with the real id; never catch-and-default. const residency: RunOpsResidency = classify(input.waitpointId); const pinnedReason = applyPinningRules(input); diff --git a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts index b552b4de736..e3e2e2c1f99 100644 --- a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts @@ -3,10 +3,10 @@ import { batchIdForMintKind, resolveBatchMintKind } from "./mintBatchFriendlyId. import { classifyKind } from "@trigger.dev/core/v3/isomorphic"; describe("batchIdForMintKind (pure)", () => { - it("ksuid -> 27-char classifiable NEW batch id (no 21-char ids)", () => { + it("ksuid -> 26-char classifiable NEW batch id (no 21-char ids)", () => { const r = batchIdForMintKind("ksuid"); expect(r.friendlyId.startsWith("batch_")).toBe(true); - expect(r.id.length).toBe(27); + expect(r.id.length).toBe(26); expect(classifyKind(r.id)).toBe("ksuid"); expect(classifyKind(r.friendlyId)).toBe("ksuid"); }); @@ -20,7 +20,7 @@ describe("batchIdForMintKind (pure)", () => { it("never mints a 21-char id", () => { for (const kind of ["cuid", "ksuid"] as const) { - expect([25, 27]).toContain(batchIdForMintKind(kind).id.length); + expect([25, 26]).toContain(batchIdForMintKind(kind).id.length); } }); }); @@ -52,7 +52,7 @@ describe("resolveBatchMintKind", () => { }); it("CHILD batch inherits a ksuid (NEW) parent by id-shape", async () => { - const parentRunFriendlyId = `run_${"a".repeat(27)}`; + const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`; const resolveRunIdMintKind = vi.fn(); const kind = await resolveBatchMintKind({ @@ -94,7 +94,7 @@ describe("resolveBatchMintKind", () => { }); it("FLIP ksuid->cuid: a ksuid (NEW) parent still mints a ksuid child though the flag now says cuid", async () => { - const parentRunFriendlyId = `run_${"a".repeat(27)}`; + const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`; const resolveRunIdMintKind = vi.fn().mockResolvedValue("cuid"); // flag flipped back to cuid const kind = await resolveBatchMintKind({ environment, diff --git a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts index 0503fc5b2c8..86e73532235 100644 --- a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts @@ -1,4 +1,4 @@ -import { BatchId, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { BatchId, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { resolveRunIdMintKind as defaultResolveRunIdMintKind, type RunIdMintKind, @@ -15,7 +15,7 @@ const defaultDeps: ResolveDeps = { export function batchIdForMintKind(kind: RunIdMintKind): { id: string; friendlyId: string } { if (kind === "ksuid") { - const id = generateKsuidId(); + const id = generateRunOpsId(); return { id, friendlyId: BatchId.toFriendlyId(id) }; } return BatchId.generate(); diff --git a/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts b/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts index 1fe52189c83..f7f7c43a530 100644 --- a/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts @@ -10,9 +10,9 @@ import { readThroughRun, type ReadThroughResult } from "./readThrough.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body → LEGACY residency. 27-char body → NEW residency. +// 25-char cuid body → LEGACY residency. 26-char v1 body (version "1" at index 25) → NEW residency. const LEGACY_RUN_ID = "run_" + "a".repeat(25); -const NEW_RUN_ID = "run_" + "b".repeat(27); +const NEW_RUN_ID = "run_" + "b".repeat(24) + "01"; // Lightweight real read: a trivial `$queryRaw` that genuinely hits the given container. // `hit` controls whether the read "finds" the run, so we exercise routing without diff --git a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts index 74baee1bb6e..9fbd5c640a8 100644 --- a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { resolveInheritedMintKind } from "./resolveInheritedMintKind.server"; -const NEW_PARENT = `run_${"a".repeat(27)}`; // ksuid id-shape -> NEW +const NEW_PARENT = `run_${"a".repeat(24) + "01"}`; // ksuid id-shape -> NEW const LEGACY_PARENT = `run_${"b".repeat(25)}`; // cuid id-shape -> LEGACY describe("resolveInheritedMintKind (pure id-shape, shared across all mint paths)", () => { diff --git a/apps/webapp/app/v3/runStore.server.test.ts b/apps/webapp/app/v3/runStore.server.test.ts index 065d8dbbc2e..ea8d9171974 100644 --- a/apps/webapp/app/v3/runStore.server.test.ts +++ b/apps/webapp/app/v3/runStore.server.test.ts @@ -6,9 +6,9 @@ import { buildRunStore } from "./runStore.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char internal id -> cuid -> LEGACY; 27-char internal id -> ksuid -> NEW. +// 25-char internal id -> cuid -> LEGACY; v1 body (version "1" at index 25) -> NEW. const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const NEW_ID_26 = "k".repeat(24) + "01"; async function seedEnvironment(prisma: PrismaClient, slugSuffix: string) { const organization = await prisma.organization.create({ @@ -88,10 +88,10 @@ describe("T24 — findRun resolves ksuid run on dedicated DB", () => { const WORKER_ID = "worker_t24_lock"; await prisma17.taskRun.create({ data: { - id: KSUID_27, + id: NEW_ID_26, engine: "V2", status: "EXECUTING", - friendlyId: "run_t24_ksuid", + friendlyId: `run_${NEW_ID_26}`, runtimeEnvironmentId: ENV_ID, environmentType: "DEVELOPMENT", organizationId: "org_t24", @@ -118,13 +118,13 @@ describe("T24 — findRun resolves ksuid run on dedicated DB", () => { }); const run = await store.findRun( - { friendlyId: "run_t24_ksuid", runtimeEnvironmentId: ENV_ID }, + { friendlyId: `run_${NEW_ID_26}`, runtimeEnvironmentId: ENV_ID }, { select: { lockedToVersionId: true } } ); expect(run).not.toBeNull(); expect(run?.lockedToVersionId).toBe(WORKER_ID); - expect(await prisma14.taskRun.findUnique({ where: { id: KSUID_27 } })).toBeNull(); + expect(await prisma14.taskRun.findUnique({ where: { id: NEW_ID_26 } })).toBeNull(); } ); }); @@ -148,7 +148,7 @@ describe("buildRunStore", () => { const seed = await seedEnvironment(prisma14, "off"); // A ksuid id (would route to NEW under split) must still land on the single DB. - const runId = KSUID_27; + const runId = NEW_ID_26; await store.createRun( createRunInput({ runId, @@ -186,15 +186,15 @@ describe("buildRunStore", () => { // ksuid -> NEW (PG17) await store.createRun( createRunInput({ - runId: KSUID_27, + runId: NEW_ID_26, friendlyId: "run_new", organizationId: seedNew.organization.id, projectId: seedNew.project.id, runtimeEnvironmentId: seedNew.environment.id, }) ); - expect(await prisma17.taskRun.findUnique({ where: { id: KSUID_27 } })).not.toBeNull(); - expect(await prisma14.taskRun.findUnique({ where: { id: KSUID_27 } })).toBeNull(); + expect(await prisma17.taskRun.findUnique({ where: { id: NEW_ID_26 } })).not.toBeNull(); + expect(await prisma14.taskRun.findUnique({ where: { id: NEW_ID_26 } })).toBeNull(); // cuid -> LEGACY (PG14) await store.createRun( diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index 62778778969..8e4ec92fae9 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -12,7 +12,7 @@ import { Prisma, } from "@trigger.dev/database"; import type { RunStore } from "@internal/run-store"; -import { generateKsuidId, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, RunId } from "@trigger.dev/core/v3/isomorphic"; import { z } from "zod"; import type { PrismaClientOrTransaction } from "~/db.server"; import { prisma } from "~/db.server"; @@ -350,7 +350,8 @@ export class BatchTriggerV3Service extends BaseService { // Mirrors RunEngineTriggerTaskService.mintRunFriendlyId. private async mintChildFriendlyId( environment: AuthenticatedEnvironment, - anchorFriendlyId?: string + anchorFriendlyId?: string, + region?: string ): Promise { const mintKind = anchorFriendlyId ? resolveInheritedMintKind(anchorFriendlyId) @@ -361,7 +362,7 @@ export class BatchTriggerV3Service extends BaseService { }); return mintKind === "ksuid" - ? RunId.toFriendlyId(generateKsuidId()) + ? RunId.toFriendlyId(generateRunOpsId(region)) : RunId.generate().friendlyId; } @@ -379,7 +380,7 @@ export class BatchTriggerV3Service extends BaseService { if (body?.dependentAttempt) { return Promise.all( body.items.map(async (item) => ({ - id: await this.mintChildFriendlyId(environment, childAnchor), + id: await this.mintChildFriendlyId(environment, childAnchor, item.options?.region), isCached: false, idempotencyKey: undefined, taskIdentifier: item.task, @@ -441,7 +442,7 @@ export class BatchTriggerV3Service extends BaseService { expiredRunIds.add(cachedRun.friendlyId); return { - id: await this.mintChildFriendlyId(environment, childAnchor), + id: await this.mintChildFriendlyId(environment, childAnchor, item.options?.region), isCached: false, idempotencyKey: item.options?.idempotencyKey ?? undefined, taskIdentifier: item.task, @@ -457,7 +458,7 @@ export class BatchTriggerV3Service extends BaseService { } return { - id: await this.mintChildFriendlyId(environment, childAnchor), + id: await this.mintChildFriendlyId(environment, childAnchor, item.options?.region), isCached: false, idempotencyKey: item.options?.idempotencyKey ?? undefined, taskIdentifier: item.task, diff --git a/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts b/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts index df6b74753ee..99d4cfd2dd7 100644 --- a/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts +++ b/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts @@ -10,9 +10,9 @@ import { hydrateRunsAcrossSeam } from "./BulkActionV2.batchReadThrough.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body → LEGACY residency. 27-char body → NEW residency. +// 25-char cuid body → LEGACY residency. 26-char v1 body (version "1" at index 25) → NEW residency. const LEGACY_RUN_ID = "run_" + "a".repeat(25); -const NEW_RUN_ID = "run_" + "b".repeat(27); +const NEW_RUN_ID = "run_" + "b".repeat(24) + "01"; type Row = { id: string }; diff --git a/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts b/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts index f8d6dcc6555..daed805b1ae 100644 --- a/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts +++ b/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts @@ -1,4 +1,4 @@ -import { isClassifiable, ownerEngine } from "@trigger.dev/core/v3/isomorphic"; +import { ownerEngine } from "@trigger.dev/core/v3/isomorphic"; import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { marqs } from "~/v3/marqs/index.server"; @@ -76,9 +76,7 @@ export class ExecuteTasksWaitingForDeployService extends BaseService { // (it is a V1/cuid-only status — V2 uses PENDING_VERSION). Surface it loudly rather // than silently strand the run, and only mutate the LEGACY-resident runs the // control-plane client can actually reach. - const newResidentRuns = runsWaitingForDeploy.filter( - (run) => isClassifiable(run.id) && ownerEngine(run.id) === "NEW" - ); + const newResidentRuns = runsWaitingForDeploy.filter((run) => ownerEngine(run.id) === "NEW"); if (newResidentRuns.length) { logger.error( "WAITING_FOR_DEPLOY selected NEW-resident runs; skipping their control-plane status flip", diff --git a/apps/webapp/test/SpanPresenter.readthrough.test.ts b/apps/webapp/test/SpanPresenter.readthrough.test.ts index 6cf625e74fd..10a402189b2 100644 --- a/apps/webapp/test/SpanPresenter.readthrough.test.ts +++ b/apps/webapp/test/SpanPresenter.readthrough.test.ts @@ -32,10 +32,10 @@ import { SpanPresenter } from "~/presenters/v3/SpanPresenter.server"; vi.setConfig({ testTimeout: 90_000 }); -// 25-char internal id → cuid → LEGACY; 27-char internal id → ksuid → NEW (the residency +// 25-char internal id → cuid → LEGACY; v1 internal id (26 chars, version "1" at index 25) → NEW (the residency // classifier shared with the RoutingRunStore's default `ownerEngine`). const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const NEW_ID_26 = "k".repeat(24) + "01"; type SeedContext = { organizationId: string; @@ -222,11 +222,12 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { const ctxNew = await seedParents(prisma17, "partn"); await mirrorParents(prisma14, ctxNew, "partn"); // legacy run-ops + CP parents share ids - const runId = `run_${KSUID_27}`; // ksuid → NEW residency - const childMigratedId = `run_a${KSUID_27.slice(1)}`; // also NEW + const runId = `run_${NEW_ID_26}`; // ksuid → NEW residency + const childMigratedId = `run_a${NEW_ID_26.slice(1)}`; // also NEW + const parentFriendlyId = `run_p${NEW_ID_26.slice(1)}`; // v1 body → routes NEW by friendlyId await createRun(prisma17, ctxNew, { id: runId, - friendlyId: "run_parent", + friendlyId: parentFriendlyId, spanId: "span_parent", taskIdentifier: "parent-task", }); @@ -273,12 +274,12 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { // (a) run hydrated through the run-ops store (NEW), byte-identical to the source row incl. // the run-ops self-relations. const run = await presenter.findRun({ - originalRunId: "run_parent", + originalRunId: parentFriendlyId, spanId: "span_parent", environmentId: ctxNew.environmentId, }); expect(run?.id).toBe(runId); - expect(run?.friendlyId).toBe("run_parent"); + expect(run?.friendlyId).toBe(parentFriendlyId); expect(run?.taskIdentifier).toBe("parent-task"); expect(run?.runTags).toEqual(["alpha", "beta"]); // Nested run-ops self-relation resolved on the same (NEW) store. @@ -286,7 +287,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { // (b) the run does NOT exist on the CP DB — the run-ops read could only have come from the // run-ops store, never a CP join. - expect(await cp.taskRun.findFirst({ where: { friendlyId: "run_parent" } })).toBeNull(); + expect(await cp.taskRun.findFirst({ where: { friendlyId: parentFriendlyId } })).toBeNull(); // (c) the control-plane standalone reads resolve from the CP client. const region = await cp.workerInstanceGroup.findFirst({ where: { masterQueue: "main" } }); @@ -305,18 +306,18 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { const ctx = await seedParents(prisma17, "kids"); await createRun(prisma17, ctx, { - id: `run_${KSUID_27}`, + id: `run_${NEW_ID_26}`, friendlyId: "run_parent2", spanId: "span_p2", }); await createRun(prisma17, ctx, { - id: `run_b${KSUID_27.slice(1)}`, + id: `run_b${NEW_ID_26.slice(1)}`, friendlyId: "run_kid_a", spanId: "span_kid_a", parentSpanId: "span_p2", }); await createRun(prisma17, ctx, { - id: `run_c${KSUID_27.slice(1)}`, + id: `run_c${NEW_ID_26.slice(1)}`, friendlyId: "run_kid_b", spanId: "span_kid_b", parentSpanId: "span_p2", @@ -384,7 +385,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { async ({ prisma14, prisma17 }) => { const ctx = await seedParents(prisma17, "knownmig"); - const newRunId = `run_${KSUID_27}`; // ksuid → NEW residency + const newRunId = `run_${NEW_ID_26}`; // ksuid → NEW residency await createRun(prisma17, ctx, { id: newRunId, friendlyId: "run_known_new", @@ -425,7 +426,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { const cp = prisma14; const ctx = await seedParents(prisma14, "passthru"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await createRun(prisma14, ctx, { id: runId, friendlyId: "run_solo", @@ -433,7 +434,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { taskIdentifier: "solo-task", }); await createRun(prisma14, ctx, { - id: `run_d${KSUID_27.slice(1)}`, + id: `run_d${NEW_ID_26.slice(1)}`, friendlyId: "run_solo_kid", spanId: "span_solo_kid", parentSpanId: "span_solo", @@ -493,7 +494,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { await mirrorParents(prisma17, ctx, "e2e4"); const parentId = `run_${CUID_25}`; // cuid → LEGACY (in-retention) - const childId = `run_${KSUID_27}`; // ksuid → NEW (born-new) + const childId = `run_${NEW_ID_26}`; // ksuid → NEW (born-new) await createRun(prisma14, ctx, { id: parentId, diff --git a/apps/webapp/test/api.v1.waitpoints.tokens.test.ts b/apps/webapp/test/api.v1.waitpoints.tokens.test.ts index 0a05975c734..1bd8948d6b5 100644 --- a/apps/webapp/test/api.v1.waitpoints.tokens.test.ts +++ b/apps/webapp/test/api.v1.waitpoints.tokens.test.ts @@ -18,7 +18,7 @@ import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import { WaitpointId, RunId, - generateKsuidId, + generateRunOpsId, ownerEngine, CUID_LENGTH, } from "@trigger.dev/core/v3/isomorphic"; @@ -341,7 +341,7 @@ describe("waitpoint-token create engine seam — NEW residency via a ksuid run a try { // A NEW-classified run id (explicit ksuid), mirroring the trigger-routing helper. - const runId = RunId.toFriendlyId(generateKsuidId()); + const runId = RunId.toFriendlyId(generateRunOpsId()); expect(ownerEngine(runId)).toBe("NEW"); const env = await seedExecutingKsuidRun(p14, router, runId, "wpnew"); diff --git a/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts b/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts index 11c97dc2705..dfec32668e8 100644 --- a/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts +++ b/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts @@ -10,9 +10,9 @@ import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresent vi.setConfig({ testTimeout: 60_000 }); -// 27-char body → NEW residency (ksuid analog). 25-char body → LEGACY residency (cuid analog). +// 26-char v1 body (version "1" at index 25) → NEW residency. 25-char body → LEGACY residency (cuid analog). function newRunId(c: string) { - return c.repeat(27); + return c.repeat(24) + "01"; } // A prisma handle that throws on any access — proves the split path never reads the passthrough diff --git a/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts b/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts index 4b0957767ac..1efc3e1ed93 100644 --- a/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts @@ -19,9 +19,9 @@ import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresent vi.setConfig({ testTimeout: 60_000 }); -// 27-char body → NEW residency (ksuid analog). 25-char body → LEGACY residency (cuid analog). +// 26-char v1 body (version "1" at index 25) → NEW residency. 25-char body → LEGACY residency (cuid analog). function newRunId(c: string) { - return c.repeat(27); + return c.repeat(24) + "01"; } function legacyRunId(c: string) { return c.repeat(25); diff --git a/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts b/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts index 48e1bcc7858..8d34782e7df 100644 --- a/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts +++ b/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts @@ -1,7 +1,7 @@ import { containerTest, heteroPostgresTest } from "@internal/testcontainers"; import { PostgresRunStore } from "@internal/run-store"; import type { Prisma, PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { beforeEach, describe, expect, vi } from "vitest"; // `resolveSchedule` reads the module-level `prisma` (control-plane handle). @@ -249,10 +249,10 @@ async function seedRunWithTree( suffix: string; } ) { - const parentId = generateKsuidId(); - const rootId = generateKsuidId(); - const runId = generateKsuidId(); - const childId = generateKsuidId(); + const parentId = generateRunOpsId(); + const rootId = generateRunOpsId(); + const runId = generateRunOpsId(); + const childId = generateRunOpsId(); await seedRun(prisma, { id: rootId, @@ -330,7 +330,7 @@ describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant ); // Control-plane schedule on the SAME single client. - const scheduleId = generateKsuidId(); + const scheduleId = generateRunOpsId(); await prisma.taskSchedule.create({ data: { id: scheduleId, @@ -399,7 +399,7 @@ describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant "cp-inv" ); - const scheduleId = generateKsuidId(); + const scheduleId = generateRunOpsId(); await prisma.taskSchedule.create({ data: { id: scheduleId, @@ -410,7 +410,7 @@ describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant }, }); - const runId = generateKsuidId(); + const runId = generateRunOpsId(); await seedRun(prisma, { id: runId, friendlyId: `run_${runId}`, @@ -522,7 +522,7 @@ describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () // The control-plane project the schedule hangs off lives on PG17. const cpEnv = await seedOrgProjectEnv(prisma17, "x-cp"); - const scheduleId = generateKsuidId(); + const scheduleId = generateRunOpsId(); await prisma17.taskSchedule.create({ data: { id: scheduleId, @@ -534,7 +534,7 @@ describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () }, }); - const runId = generateKsuidId(); + const runId = generateRunOpsId(); await seedRun(prisma14, { id: runId, friendlyId: `run_${runId}`, @@ -576,7 +576,7 @@ describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () // A run that exists on NEITHER store (terminated + past-retention, // observed at this layer as a miss on both underlying stores). - const goneFriendlyId = `run_${generateKsuidId()}`; + const goneFriendlyId = `run_${generateRunOpsId()}`; const fromNew = await readFoundRunViaStore( newStore, diff --git a/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts b/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts index 3b7571f4f55..fbf685f2b16 100644 --- a/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts @@ -21,7 +21,7 @@ vi.setConfig({ testTimeout: 60_000 }); const idGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", 21); -// Residency by friendlyId length (after stripping `run_`): 27-char body → NEW (ksuid analog), +// Residency by friendlyId length (after stripping `run_`): 26-char v1 body (version "1" at index 25) → NEW, // 25-char body → LEGACY (cuid analog). ownerEngine classifies on the public friendly id. function newFriendlyId(): string { return "run_" + customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 27)(); diff --git a/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts b/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts index 58d3827f55c..00b1d6ea650 100644 --- a/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts @@ -9,14 +9,14 @@ import { } from "@internal/testcontainers"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import type { PrismaClient, WaitpointType } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; import type { PrismaReplicaClient } from "~/db.server"; import { ApiWaitpointPresenter } from "~/presenters/v3/ApiWaitpointPresenter.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body (length-disjoint from the 27-char KSUID) → LEGACY residency. +// 25-char cuid body (no v1 version marker) → LEGACY residency. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -113,8 +113,8 @@ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgre heteroPostgresTest( "resolves on run-ops NEW (ksuid), legacy replica never touched", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); - expect(id.length).toBe(27); + const id = generateRunOpsId(); + expect(id.length).toBe(26); const { project, environment } = await seedOrgProjectEnv(prisma17, "new"); const seeded = await seedWaitpoint( @@ -218,7 +218,7 @@ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgre "cross-seam — new-resident served from NEW (legacy untouched); in-retention served from legacy", async ({ prisma17, prisma14 }) => { // New-resident waitpoint: lives on NEW, the new probe hits, legacy must never be touched. - const newId = generateKsuidId(); + const newId = generateRunOpsId(); const newEnv = await seedOrgProjectEnv(prisma17, "x2new"); await seedWaitpoint(prisma17, newId, { id: newEnv.environment.id, @@ -301,7 +301,7 @@ describe("ApiWaitpointPresenter passthrough (single-DB)", () => { postgresTest( "no read-through deps → one plain replica read; legacy never touched", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const { project, environment } = await seedOrgProjectEnv(prisma, "pt"); const seeded = await seedWaitpoint( prisma, diff --git a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts index d5a4f0ccda9..875ac58613d 100644 --- a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts +++ b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts @@ -13,14 +13,14 @@ vi.mock("~/db.server", () => ({ runOpsLegacyReplica: {}, })); -import { BatchId, generateKsuidId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { BatchId, generateRunOpsId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { BatchTriggerV3Service } from "~/v3/services/batchTriggerV3.server"; vi.setConfig({ testTimeout: 60_000 }); const CUID_LEN = 25; -const KSUID_LEN = 27; +const KSUID_LEN = 26; // Minimal AuthenticatedEnvironment — only the fields the mint path reads // (organizationId, id, organization.featureFlags) need to be real. A root batch @@ -45,8 +45,8 @@ describe("BatchTriggerV3Service child-residency inheritance", () => { it("a ksuid parent yields ksuid (NEW) child friendlyIds", async () => { const service = buildService(); const parentFriendlyId = RunId.toFriendlyId( - // 27-char ksuid internal id → NEW residency parent - "a".repeat(KSUID_LEN) + // v1 internal id (version "1" at index 25) → NEW residency parent + "a".repeat(KSUID_LEN - 1) + "1" ); expect(ownerEngine(RunId.fromFriendlyId(parentFriendlyId))).toBe("NEW"); @@ -80,7 +80,7 @@ describe("BatchTriggerV3Service child-residency inheritance", () => { // batch + children stay co-resident and TaskRun.batchId never crosses the seam. it("a ksuid batch anchor yields ksuid children even when the env flag resolves cuid", async () => { const service = buildService(); // resolveMintKind forced to "cuid" - const batchFriendlyId = BatchId.toFriendlyId(generateKsuidId()); // ksuid (NEW) batch + const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId()); // ksuid (NEW) batch expect(ownerEngine(batchFriendlyId)).toBe("NEW"); const childFriendlyId = await (service as any).mintChildFriendlyId(fakeEnv(), batchFriendlyId); diff --git a/apps/webapp/test/batchTriggerV3StoreRouting.test.ts b/apps/webapp/test/batchTriggerV3StoreRouting.test.ts index 5e1f60d4de6..39b7e2766aa 100644 --- a/apps/webapp/test/batchTriggerV3StoreRouting.test.ts +++ b/apps/webapp/test/batchTriggerV3StoreRouting.test.ts @@ -1,7 +1,7 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import { PostgresRunStore } from "@internal/run-store"; import { isUniqueConstraintError, type PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; vi.setConfig({ testTimeout: 60_000 }); @@ -50,7 +50,7 @@ async function seedRun( idempotencyKeyExpiresAt?: Date; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, @@ -74,7 +74,7 @@ async function seedRun( } async function seedBatch(prisma: PrismaClient, runtimeEnvironmentId: string, suffix: string) { - const batchId = generateKsuidId(); + const batchId = generateRunOpsId(); return prisma.batchTaskRun.create({ data: { id: batchId, diff --git a/apps/webapp/test/bulkActionV2ReadRouting.test.ts b/apps/webapp/test/bulkActionV2ReadRouting.test.ts index 314a6df6ca2..3b9398e794b 100644 --- a/apps/webapp/test/bulkActionV2ReadRouting.test.ts +++ b/apps/webapp/test/bulkActionV2ReadRouting.test.ts @@ -16,9 +16,9 @@ import { hydrateRunsAcrossSeam } from "~/v3/services/bulk/BulkActionV2.batchRead vi.setConfig({ testTimeout: 60_000 }); -// 27-char body → NEW residency (ksuid analog). 25-char body → LEGACY residency (cuid analog). +// 26-char v1 body (version "1" at index 25) → NEW residency. 25-char body → LEGACY residency (cuid analog). function newId(c: string) { - return "run_" + c.repeat(27); + return "run_" + c.repeat(24) + "01"; } function legacyId(c: string) { return "run_" + c.repeat(25); diff --git a/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts b/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts index ea29821fd19..55fc919558b 100644 --- a/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts +++ b/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts @@ -3,14 +3,14 @@ // splitEnabled boundary and recording client wrappers are injected. import { heteroPostgresTest, postgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; import type { PrismaReplicaClient } from "~/db.server"; import { CancelDevSessionRunsService } from "~/v3/services/cancelDevSessionRuns.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body (length-disjoint from the 27-char KSUID) → LEGACY residency. +// 25-char cuid body (no v1 version marker) → LEGACY residency. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -92,8 +92,8 @@ describe("CancelDevSessionRunsService store routing (hetero)", () => { heteroPostgresTest( "a NEW run (ksuid) resolves on the new store via read-through, by friendlyId and by id", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); - expect(id.length).toBe(27); + const id = generateRunOpsId(); + expect(id.length).toBe(26); const friendlyId = `run_${id}`; const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv( @@ -204,7 +204,7 @@ describe("CancelDevSessionRunsService passthrough (single-DB)", () => { postgresTest( "with no read-through deps, the run is read from the single DB and session reads stay on it", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv(prisma, "pt"); diff --git a/apps/webapp/test/crossSeamGuard.proof.test.ts b/apps/webapp/test/crossSeamGuard.proof.test.ts index ec59649dd41..2ed27ed7b68 100644 --- a/apps/webapp/test/crossSeamGuard.proof.test.ts +++ b/apps/webapp/test/crossSeamGuard.proof.test.ts @@ -13,7 +13,7 @@ import { } from "~/v3/runOpsMigration/unblockRouteCatalog"; import { WaitpointId } from "@trigger.dev/core/v3/isomorphic"; -const NEW_WP = WaitpointId.toFriendlyId("0".repeat(27)); // 27-char internal body → NEW +const NEW_WP = WaitpointId.toFriendlyId("0".repeat(24) + "01"); // v1 internal body → NEW const LEGACY_WP = WaitpointId.toFriendlyId("c".repeat(25)); // 25-char internal body → LEGACY describe("cross-seam guard — exhaustive per-route store selection", () => { @@ -163,7 +163,7 @@ describe("cross-seam guard — PG14+PG17 hetero-fixture proof", () => { heteroPostgresTest( "exhaustive routes resolve to the physically-correct store on PG14+PG17", async ({ prisma14, prisma17 }) => { - const newWp = WaitpointId.toFriendlyId("0".repeat(27)); // NEW → PG17 + const newWp = WaitpointId.toFriendlyId("0".repeat(24) + "01"); // NEW → PG17 const legacyWp = WaitpointId.toFriendlyId("c".repeat(25)); // LEGACY → PG14 // Distinct parent chains per residency; each Waitpoint lives on its own DB diff --git a/apps/webapp/test/engine/triggerFailedTask.test.ts b/apps/webapp/test/engine/triggerFailedTask.test.ts index ab6951a5701..17568959293 100644 --- a/apps/webapp/test/engine/triggerFailedTask.test.ts +++ b/apps/webapp/test/engine/triggerFailedTask.test.ts @@ -4,7 +4,7 @@ import { RunEngine } from "@internal/run-engine"; import { setupAuthenticatedEnvironment, setupBackgroundWorker } from "@internal/run-engine/tests"; import { containerTest } from "@internal/testcontainers"; import { trace } from "@opentelemetry/api"; -import { RunId, classifyKind, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { RunId, classifyKind, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { TriggerFailedTaskService } from "../../app/runEngine/services/triggerFailedTask.server"; import { EventRepository } from "../../app/v3/eventRepository/eventRepository.server"; @@ -93,7 +93,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { const taskIdentifier = "failed-residency-task"; await setupBackgroundWorker(engine, environment, taskIdentifier); - const parentFriendlyId = RunId.toFriendlyId(generateKsuidId()); + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); expect(classifyKind(parentFriendlyId)).toBe("ksuid"); await engine.trigger( { @@ -189,7 +189,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { const taskIdentifier = "failed-residency-task"; await setupBackgroundWorker(engine, environment, taskIdentifier); - const parentFriendlyId = RunId.toFriendlyId(generateKsuidId()); + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); await engine.trigger( { friendlyId: parentFriendlyId, @@ -238,7 +238,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { // A well-formed ksuid parent friendlyId that was NEVER triggered → no row. // Exercises the missing-parent fallback in callWithoutTraceEvents. - const absentParentFriendlyId = RunId.toFriendlyId(generateKsuidId()); + const absentParentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); const friendlyId = await service.callWithoutTraceEvents({ environmentId: environment.id, diff --git a/apps/webapp/test/engine/triggerTask.test.ts b/apps/webapp/test/engine/triggerTask.test.ts index ba1452abceb..e95a7e746e5 100644 --- a/apps/webapp/test/engine/triggerTask.test.ts +++ b/apps/webapp/test/engine/triggerTask.test.ts @@ -30,7 +30,7 @@ import { RunId, classifyKind, generateInternalId, - generateKsuidId, + generateRunOpsId, } from "@trigger.dev/core/v3/isomorphic"; import type { TaskRun } from "@trigger.dev/database"; import { Redis } from "ioredis"; @@ -2396,8 +2396,8 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = // the runFriendlyId option so the row physically exists for the parent // lookup the child path performs. const parentFriendlyId = RunId.toFriendlyId( - // 27-char ksuid → classifies NEW - (await import("@trigger.dev/core/v3/isomorphic")).generateKsuidId() + // v1 id (version "1" at index 25) → classifies NEW + (await import("@trigger.dev/core/v3/isomorphic")).generateRunOpsId() ); expect(classifyKind(parentFriendlyId)).toBe("ksuid"); @@ -2429,7 +2429,7 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = // Explicit cuid id for the run, and a ksuid/NEW parent id. const explicitFriendlyId = RunId.toFriendlyId(generateInternalId()); - const parentFriendlyId = RunId.toFriendlyId(generateKsuidId()); + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); expect(classifyKind(explicitFriendlyId)).toBe("cuid"); expect(classifyKind(parentFriendlyId)).toBe("ksuid"); diff --git a/apps/webapp/test/idempotencyDedupResidency.test.ts b/apps/webapp/test/idempotencyDedupResidency.test.ts index d8ab8d934cb..38cfc2abf31 100644 --- a/apps/webapp/test/idempotencyDedupResidency.test.ts +++ b/apps/webapp/test/idempotencyDedupResidency.test.ts @@ -1,6 +1,6 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; // Stub so the runStore singleton doesn't eagerly connect at import. @@ -79,7 +79,7 @@ async function seedRun( status?: "PENDING" | "EXECUTING" | "COMPLETED_SUCCESSFULLY" | "COMPLETED_WITH_ERRORS"; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, diff --git a/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts b/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts index 5434567d42e..f4de27e6f29 100644 --- a/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts +++ b/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts @@ -1,6 +1,6 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; // Stub `~/db.server` so the `runStore` singleton doesn't eagerly connect at @@ -97,7 +97,7 @@ async function seedRun( idempotencyKeyExpiresAt?: Date; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, diff --git a/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts b/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts index 707a3546cf8..ed787232c7a 100644 --- a/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts +++ b/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts @@ -10,7 +10,7 @@ import { heteroPostgresTest, postgresTest } from "@internal/testcontainers"; import { PostgresRunStore } from "@internal/run-store"; import type { ReadClient, RunStore } from "@internal/run-store"; import type { Prisma, PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, ownerEngine } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect } from "vitest"; import { ControlPlaneCache } from "~/v3/runOpsMigration/controlPlaneCache.server"; import { ControlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server"; @@ -41,7 +41,7 @@ class RoutingRunStore implements RunStore { } #resolveById(runId: string): PostgresRunStore { - return runId.length === 27 ? this.#newStore : this.#legacyStore; + return ownerEngine(runId) === "NEW" ? this.#newStore : this.#legacyStore; } #idFromWhere(where: Prisma.TaskRunWhereInput): string | undefined { @@ -220,7 +220,7 @@ describe("PerformTaskRunAlertsService store routing (hetero)", () => { heteroPostgresTest( "env type resolves via the control-plane resolver (distinct DB) while the run resolves on the run-ops store", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; // Cloud shape: run-ops = the new DB (cross-seam FKs dropped), control-plane = the legacy DB. @@ -312,7 +312,7 @@ describe("PerformTaskRunAlertsService passthrough (single-DB)", () => { postgresTest( "with the default store, run read + alert-channel read both resolve on the single DB", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; const { project, organization, runtimeEnvironment } = await seedProject(prisma, "pt"); diff --git a/apps/webapp/test/realtime/runReaderReadThrough.test.ts b/apps/webapp/test/realtime/runReaderReadThrough.test.ts index 88c4e11e27c..978f3eedc44 100644 --- a/apps/webapp/test/realtime/runReaderReadThrough.test.ts +++ b/apps/webapp/test/realtime/runReaderReadThrough.test.ts @@ -18,13 +18,13 @@ import { RunHydrator } from "~/services/realtime/runReader.server"; // The heterogeneous fixture gives real legacy + new Postgres containers; NO DB is mocked. The ONLY // non-DB fake is the residency selector that the routing-shaped store uses (`ownerEngine`: ksuid -> // NEW, cuid -> LEGACY), exactly the substrate the RoutingRunStore ships. Run ids are 25 chars (cuid -// -> LEGACY) or 27 chars (ksuid -> NEW) so the classifier routes them deterministically. +// -> LEGACY) or v1-shaped (26 chars, version "1" at index 25 -> NEW) so the classifier routes them deterministically. -// 25-char internal id -> cuid -> LEGACY; 27-char internal id -> ksuid -> NEW. The +// 25-char internal id -> cuid -> LEGACY; v1 internal id (26 chars, version "1" at index 25) -> NEW. The // classifier strips a leading `_`, so these ids must carry NO underscore (a bare // alphanumeric body of the exact length). function newId(label: string): string { - return ("k" + label.replace(/[^a-z0-9]/gi, "")).padEnd(27, "0").slice(0, 27); + return ("k" + label.replace(/[^0-9a-v]/g, "")).padEnd(24, "0").slice(0, 24) + "01"; } function legacyId(label: string): string { return ("c" + label.replace(/[^a-z0-9]/gi, "")).padEnd(25, "0").slice(0, 25); diff --git a/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts b/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts index 04c442ca187..df1264022ab 100644 --- a/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts +++ b/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts @@ -1,6 +1,6 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; // Stub these so the default singletons don't eagerly connect at import. The @@ -76,7 +76,7 @@ async function seedRun( idempotencyKeyExpiresAt?: Date; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, diff --git a/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts b/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts index 3b3ec417895..fed06cb0ac6 100644 --- a/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts +++ b/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts @@ -1,14 +1,14 @@ import { heteroRunOpsPostgresTest, postgresTest } from "@internal/testcontainers"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; import type { PrismaReplicaClient } from "~/db.server"; import { resolveWaitpointThroughReadThrough } from "~/runEngine/concerns/resolveWaitpointThroughReadThrough.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid (length-disjoint from the 27-char KSUID) -> LEGACY residency. +// 25-char cuid (no v1 version marker) -> LEGACY residency. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -86,13 +86,13 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run heteroRunOpsPostgresTest( "ksuid waitpoint resolves on the dedicated run-ops client; legacy replica never touched", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); - expect(id.length).toBe(27); + const id = generateRunOpsId(); + expect(id.length).toBe(26); // The dedicated run-ops DB has no control-plane tables; the waitpoint's // environment/project FKs are synthetic scalar ids. - const environmentId = generateKsuidId(); - const projectId = generateKsuidId(); + const environmentId = generateRunOpsId(); + const projectId = generateRunOpsId(); const seeded = await seedWaitpoint(prisma17, id, { id: environmentId, projectId }); const newClient = recording(prisma17); @@ -156,10 +156,10 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run async ({ prisma17, prisma14 }) => { // The bare wait route passes NO `deps`; the `defaults` DI seam models old vs new // fallback against containers, avoiding the real db.server topology. - const id = generateKsuidId(); - expect(id.length).toBe(27); - const environmentId = generateKsuidId(); - const projectId = generateKsuidId(); + const id = generateRunOpsId(); + expect(id.length).toBe(26); + const environmentId = generateRunOpsId(); + const projectId = generateRunOpsId(); const seeded = await seedWaitpoint(prisma17, id, { id: environmentId, projectId }); // FAIL-BEFORE: old default pinned newClient to control-plane ($replica ≈ prisma14) → miss. @@ -215,7 +215,7 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run postgresTest( "passthrough (single-DB): one plain read; legacy never invoked", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const { project, environment } = await seedOrgProjectEnv(prisma, "pt"); const seeded = await seedWaitpoint(prisma, id, { id: environment.id, diff --git a/apps/webapp/test/runOpsCrossSeamGuard.test.ts b/apps/webapp/test/runOpsCrossSeamGuard.test.ts index 5f232c08693..2f62cf01726 100644 --- a/apps/webapp/test/runOpsCrossSeamGuard.test.ts +++ b/apps/webapp/test/runOpsCrossSeamGuard.test.ts @@ -3,12 +3,10 @@ import { computeStoreForCompletion, selectStoreForWaitpoint, } from "~/v3/runOpsMigration/crossSeamGuard.server"; -import { UnclassifiableRunId } from "@trigger.dev/core/v3/isomorphic"; - // Real sample ids exercising the genuine run-id residency classifier (no stub). -const NEW = "waitpoint_" + "a".repeat(27); // 27-char ksuid body -> NEW +const NEW = "waitpoint_" + "a".repeat(24) + "01"; // v1 body (version "1" at index 25) -> NEW const LEGACY = "waitpoint_" + "a".repeat(25); // 25-char cuid body -> LEGACY -const AMBIGUOUS = "waitpoint_" + "a".repeat(10); // neither length -> throws +const UNRECOGNIZED = "waitpoint_" + "a".repeat(10); // no version marker -> LEGACY describe("selectStoreForWaitpoint — happy-path residency routing", () => { it("MANUAL completion of a NEW waitpoint selects the new store", () => { @@ -92,11 +90,11 @@ describe("selectStoreForWaitpoint — legacy pins", () => { }); }); -describe("selectStoreForWaitpoint — ambiguity and unknown routes are loud", () => { - it("rethrows UnclassifiableRunId for an ambiguous-length id (never silently routes)", () => { - expect(() => selectStoreForWaitpoint({ waitpointId: AMBIGUOUS, routeKind: "MANUAL" })).toThrow( - UnclassifiableRunId - ); +describe("selectStoreForWaitpoint — unrecognized shapes and unknown routes", () => { + it("routes an id without the v1 version marker to legacy (classification is total)", () => { + const d = selectStoreForWaitpoint({ waitpointId: UNRECOGNIZED, routeKind: "MANUAL" }); + expect(d.store).toBe("legacy"); + expect(d.residency).toBe("LEGACY"); }); it("throws when an unknown routeKind is supplied", () => { @@ -111,7 +109,7 @@ describe("computeStoreForCompletion — single-DB no-op + flag wrapper", () => { it("returns the single store without classifying when split is OFF", () => { const calls: string[] = []; const d = computeStoreForCompletion( - { waitpointId: AMBIGUOUS, routeKind: "MANUAL" }, + { waitpointId: UNRECOGNIZED, routeKind: "MANUAL" }, { splitEnabled: false, classify: (id) => { diff --git a/apps/webapp/test/runOpsMintCutover.test.ts b/apps/webapp/test/runOpsMintCutover.test.ts index d838a382e92..ca7cbb6b9c9 100644 --- a/apps/webapp/test/runOpsMintCutover.test.ts +++ b/apps/webapp/test/runOpsMintCutover.test.ts @@ -5,10 +5,10 @@ // `FeatureFlag` rows in a testcontainers Postgres. Only the two boundary knobs // are injected — `masterEnabled` and the `splitEnabled` boot-boolean — never a // mocked DB. The KSUID/cuid format + residency are then proven through the SAME isomorphic -// helpers the real trigger path uses (`generateKsuidId` / `RunId.toFriendlyId` / +// helpers the real trigger path uses (`generateRunOpsId` / `RunId.toFriendlyId` / // `RunId.fromFriendlyId` / `ownerEngine`). import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; import { postgresTest } from "@internal/testcontainers"; import { describe, expect, vi } from "vitest"; import { @@ -28,7 +28,7 @@ vi.setConfig({ testTimeout: 60_000 }); // The real trigger-path mint helper, copied verbatim from triggerTask.server.ts so the // test exercises the exact id format a cut-over env produces. function mintRunKsuidFriendlyId(): string { - return RunId.toFriendlyId(generateKsuidId()); + return RunId.toFriendlyId(generateRunOpsId()); } // Mirrors the real trigger path: resolve the kind, then mint either a KSUID friendlyId or @@ -102,7 +102,7 @@ describe("per-env KSUID mint cutover", () => { const friendlyA = mintRunFriendlyId(kindA); const friendlyB = mintRunFriendlyId(kindB); - expect(RunId.fromFriendlyId(friendlyA).length).toBe(27); + expect(RunId.fromFriendlyId(friendlyA).length).toBe(26); expect(ownerEngine(RunId.fromFriendlyId(friendlyA))).toBe("NEW"); expect(RunId.fromFriendlyId(friendlyB).length).toBe(25); @@ -159,7 +159,7 @@ describe("per-env KSUID mint cutover", () => { expect(ownerEngine(RunId.fromFriendlyId(nextFriendly))).toBe("LEGACY"); // The already-minted KSUID run is untouched — drain-new-forward never reverts it. - expect(RunId.fromFriendlyId(firstFriendly).length).toBe(27); + expect(RunId.fromFriendlyId(firstFriendly).length).toBe(26); expect(ownerEngine(RunId.fromFriendlyId(firstFriendly))).toBe("NEW"); } ); diff --git a/apps/webapp/test/runPresenterReadRoute.test.ts b/apps/webapp/test/runPresenterReadRoute.test.ts index e3733ff8f27..bf3a92ca30a 100644 --- a/apps/webapp/test/runPresenterReadRoute.test.ts +++ b/apps/webapp/test/runPresenterReadRoute.test.ts @@ -6,7 +6,7 @@ // `user.findFirst` admin read — keeping the test off ClickHouse. import { postgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; vi.setConfig({ testTimeout: 60_000 }); @@ -172,7 +172,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { suffix ); - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; const run = await seedRun( prisma, @@ -216,7 +216,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { suffix ); - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; await seedRun( prisma, @@ -270,7 +270,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { const suffix = uniqueSuffix("notfound"); const { user, project, runtimeEnvironment } = await seedOrgProjectEnvMember(prisma, suffix); - const missingFriendlyId = `run_${generateKsuidId()}`; + const missingFriendlyId = `run_${generateRunOpsId()}`; const presenter = new RunPresenter(prisma); await expect( @@ -294,7 +294,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { suffix ); - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; await seedRun( prisma, diff --git a/apps/webapp/test/runsRepository.readthrough.test.ts b/apps/webapp/test/runsRepository.readthrough.test.ts index fd3f342f9c6..edc8d513fc1 100644 --- a/apps/webapp/test/runsRepository.readthrough.test.ts +++ b/apps/webapp/test/runsRepository.readthrough.test.ts @@ -367,11 +367,11 @@ describe("RunsRepository read-through id-set hydrate (PG14 legacy + PG17 new)", const ctx = await seedParents(prisma, "keysetwalk"); - // cuid-shaped ids (25 chars, "c" prefix) and ksuid-shaped ids (27 chars, "2" prefix). Lexical + // cuid-shaped ids (25 chars, "c" prefix) and v1-shaped ids (26 chars, "2" prefix, version "1"). Lexical // `id desc` groups all "c" ids ahead of all "2" ids; the created_at order below interleaves // them, so the two orders genuinely differ across the seam. const cuid = (n: number) => `c${String(n).padStart(24, "0")}`; - const ksuid = (n: number) => `2${String(n).padStart(26, "0")}`; + const ksuid = (n: number) => `2${String(n).padStart(23, "0")}01`; // created_at DESC order (index 0 = most recent) interleaves the id-spaces: ksuid, cuid, // ksuid, cuid, ksuid, cuid. diff --git a/apps/webapp/test/sessions.readthrough.test.ts b/apps/webapp/test/sessions.readthrough.test.ts index 6496baeb16f..23410c29f6c 100644 --- a/apps/webapp/test/sessions.readthrough.test.ts +++ b/apps/webapp/test/sessions.readthrough.test.ts @@ -11,7 +11,7 @@ vi.mock("~/db.server", () => ({ import { heteroRunOpsPostgresTest, postgresTest } from "@internal/testcontainers"; import { buildRunStore } from "~/v3/runStore.server"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import type { PrismaClient } from "@trigger.dev/database"; import { @@ -259,7 +259,7 @@ describe("sessions serializer currentRunId resolution", () => { const newRun = await createNewRun(prisma17, ctx, { friendlyId: "run_new", - id: generateKsuidId(), + id: generateRunOpsId(), }); const legacyRun = await createLegacyRun(prisma14, ctx, { friendlyId: "run_legacy" }); @@ -299,7 +299,7 @@ describe("sessions serializer currentRunId resolution", () => { const newRun = await createNewRun(prisma17, ctx, { friendlyId: "run_bnew", - id: generateKsuidId(), + id: generateRunOpsId(), }); const legacyRun = await createLegacyRun(prisma14, ctx, { friendlyId: "run_blegacy" }); const crossEnvRun = await createLegacyRun(prisma14, otherCtx, { friendlyId: "run_bcross" }); diff --git a/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts b/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts index eded6bccf99..453468eb768 100644 --- a/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts +++ b/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts @@ -3,7 +3,7 @@ import { PostgresRunStore } from "@internal/run-store"; import type { ReadClient, RunStore } from "@internal/run-store"; import type { Prisma, PrismaClient } from "@trigger.dev/database"; import { parsePacket } from "@trigger.dev/core/v3"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, ownerEngine } from "@trigger.dev/core/v3/isomorphic"; import { setTimeout } from "timers/promises"; import { describe, expect } from "vitest"; import { UpdateMetadataService } from "~/services/metadata/updateMetadata.server"; @@ -22,7 +22,7 @@ vi.setConfig({ testTimeout: 60_000 }); * store by id length, then calls the inner store WITHOUT forwarding the outer tx * (passes undefined), so the inner PostgresRunStore uses its own prisma17/prisma14. * - * Classification contract (length-disjoint): 27-char id => KSUID => NEW store; + * Classification contract (version char): a v1 id (26 chars, version "1" at index 25) => NEW store; * 25-char cuid => LEGACY store. */ class RoutingRunStore implements RunStore { @@ -34,13 +34,13 @@ class RoutingRunStore implements RunStore { this.#legacyStore = legacyStore; } - // Resolve by run-id length: 27 => NEW (KSUID), otherwise LEGACY (25-char cuid). + // Resolve by the version char: a v1 body => NEW, otherwise LEGACY (25-char cuid). #resolveById(runId: string): PostgresRunStore { - return runId.length === 27 ? this.#newStore : this.#legacyStore; + return ownerEngine(runId) === "NEW" ? this.#newStore : this.#legacyStore; } // Extract a classifiable run id from a `where`. Prefers `where.id`; if only a - // friendlyId is present we cannot classify by length, so the caller falls back + // friendlyId is present the stub does not classify, so the caller falls back // to read-through (try NEW, then LEGACY). #idFromWhere(where: Prisma.TaskRunWhereInput): string | undefined { const id = (where as { id?: unknown }).id; @@ -56,7 +56,7 @@ class RoutingRunStore implements RunStore { ): Promise { const id = this.#idFromWhere(where); if (id !== undefined) { - // Classifiable by id length — route to the owning store, dropping the + // Classifiable by id shape — route to the owning store, dropping the // forwarded client so the inner store uses its OWN prisma. return (this.#resolveById(id).findRun as any)(where, argsOrClient); } @@ -222,7 +222,7 @@ function buildRoutingStore(prisma17: PrismaClient, prisma14: PrismaClient) { return new RoutingRunStore(newStore, legacyStore); } -// 25-char cuid-format id (starts with 'c'), length-disjoint from the 27-char KSUID. +// 25-char cuid-format id (starts with "c"), no v1 version marker. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -261,8 +261,8 @@ describe("UpdateMetadataService store routing (hetero)", () => { heteroPostgresTest( "routes read+CAS to the owning (NEW/PG17) store for a KSUID run", async ({ prisma17, prisma14 }) => { - const runId = generateKsuidId(); - expect(runId.length).toBe(27); + const runId = generateRunOpsId(); + expect(runId.length).toBe(26); const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv( prisma17, @@ -326,8 +326,8 @@ describe("UpdateMetadataService store routing (hetero)", () => { heteroPostgresTest( "preserves CAS under concurrent writers on a NEW-DB (PG17) run", async ({ prisma17, prisma14 }) => { - const runId = generateKsuidId(); - expect(runId.length).toBe(27); + const runId = generateRunOpsId(); + expect(runId.length).toBe(26); const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv( prisma17, diff --git a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts index c52291876c7..aeb361019e6 100644 --- a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts +++ b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts @@ -345,7 +345,7 @@ describe("executionSnapshotSystem store routing (cross-version read-through)", ( } ); - // Routing keys off runId, the SnapshotId is a cuid (not a 27-char ksuid), and no + // Routing keys off runId, the SnapshotId is a cuid (not a v1 run-ops id), and no // residency classifier is consulted for the snapshot id (D5). heteroPostgresTest( "snapshots route by owning run id; SnapshotId stays cuid", @@ -356,7 +356,7 @@ describe("executionSnapshotSystem store routing (cross-version read-through)", ( const rNew = "run_new_f"; const { snapshot } = await seedRunWithSnapshot(prisma17 as any, newStore, "new_f", rNew); - // cuid is 25 chars (c + 24); a ksuid friendly id is 27 chars. The snapshot id is a cuid. + // cuid is 25 chars (c + 24); a v1 run-ops body is 26 chars ending in "1". The snapshot id is a cuid. expect(snapshot.id.length).toBe(25); const router = new TwoStoreSnapshotRouter(newStore, legacyStore, [rNew]); diff --git a/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts b/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts index 87ad9dd3520..2e5a7347218 100644 --- a/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts +++ b/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts @@ -1396,7 +1396,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", { timeout: 15_000, interval: 100 } ); - // (c) the load-bearing no-op: an unclassifiable id (length 26) must NOT throw + // (c) the load-bearing no-op: an unrecognized id (26 "a"s, no version marker) must NOT throw // UnclassifiableWaitpointId under the default single store — the classifier is // never consulted. It finds no PENDING row, the re-read fails, and the ordinary // "Waitpoint not found" surfaces instead. @@ -1429,8 +1429,8 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", const envLegacy = await seedHeteroEnvironment(prisma14, "csl"); const envNew = await seedHeteroEnvironment(prisma17, "csn"); - // 27-char body => ksuid => NEW (dedicated run-ops DB); 25-char body => cuid => LEGACY. - const ksuidId = "waitpoint_" + "a".repeat(27); + // v1 body (26 chars, version "1" at index 25) => ksuid => NEW (dedicated run-ops DB); 25-char body => cuid => LEGACY. + const ksuidId = "waitpoint_" + "a".repeat(24) + "01"; const cuidId = "waitpoint_" + "b".repeat(25); await prisma17.waitpoint.create({ @@ -1510,7 +1510,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", const router = new RoutingRunStore({ new: newStore, legacy }); // pin is DRIVEN via explicit context at the store seam; the engine completeWaitpoint entry cannot derive it — the organic cross-tree-idempotency pin is applied at the webapp idempotency caller. - const ksuidId = "waitpoint_" + "a".repeat(27); + const ksuidId = "waitpoint_" + "a".repeat(24) + "01"; const handle = await router.forWaitpointCompletion(ksuidId, { routeKind: "IDEMPOTENCY_REUSE", isCrossTreeIdempotency: true, diff --git a/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts b/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts index f4d64a844e0..eabc9577a6f 100644 --- a/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts +++ b/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts @@ -29,9 +29,9 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (27-char internal id) → classified NEW → routed to the run-ops (#new) store. -const KSUID_A = "k".repeat(27); -const KSUID_B = "m".repeat(27); +// ksuid (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. +const KSUID_A = "k".repeat(24) + "01"; +const KSUID_B = "m".repeat(24) + "01"; function baseEngineOptions(redisOptions: any, prisma: any) { return { diff --git a/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts b/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts index dc1d7e3509b..d5df10dba4b 100644 --- a/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts +++ b/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts @@ -35,10 +35,10 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (27-char internal id) → classified NEW → routed to the run-ops (#new) store. -const KSUID_A = "n".repeat(27); +// ksuid (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. +const KSUID_A = "n".repeat(24) + "01"; // A second ksuid run for the cross-DB (NEW-run → LEGACY-token) case. -const KSUID_X = "x".repeat(27); +const KSUID_X = "k".repeat(24) + "01"; // cuid (25-char) → classified LEGACY → a standalone token resident on #legacy (prisma14). const CUID_25 = "c".repeat(25); diff --git a/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts b/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts index 04680161881..7030c0e43ff 100644 --- a/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts +++ b/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts @@ -35,11 +35,11 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (27-char internal id) → classified NEW → routed to the run-ops (#new) store. -const KSUID_A = "k".repeat(27); -const KSUID_B = "m".repeat(27); -const KSUID_C = "n".repeat(27); -const KSUID_D = "p".repeat(27); +// ksuid (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. +const KSUID_A = "k".repeat(24) + "01"; +const KSUID_B = "m".repeat(24) + "01"; +const KSUID_C = "n".repeat(24) + "01"; +const KSUID_D = "p".repeat(24) + "01"; function baseEngineOptions(redisOptions: any, prisma: any) { return { diff --git a/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts b/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts index 5bcbfac8ce3..08a3cf2260f 100644 --- a/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts +++ b/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts @@ -456,10 +456,14 @@ describe("RunEngine lifecycle read routing (single-DB)", () => { // (cuid id, on PG14) reads THROUGH the legacy store's read-only (replica) client. // --------------------------------------------------------------------------- -// A cuid-length (25-char) internal id → classifies LEGACY; a ksuid-length (27-char) +// A cuid-length (25-char) internal id → classifies LEGACY; a v1-shaped (26-char, version "1") // internal id → classifies NEW. The `run_` prefix is stripped before classification. const legacyRunId = (suffix: string) => `run_${suffix.padEnd(25, "0").slice(0, 25)}`; -const newRunId = (suffix: string) => `run_${suffix.padEnd(27, "0").slice(0, 27)}`; +const newRunId = (suffix: string) => + `run_${suffix + .replace(/[^0-9a-v]/g, "0") + .padEnd(24, "0") + .slice(0, 24)}01`; async function seedRunWithSnapshot( prisma: PrismaClient, diff --git a/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts b/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts index f27ef6b8572..ea7bbeb263d 100644 --- a/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts +++ b/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts @@ -7,7 +7,7 @@ import { type CreateFailedRunInput, type CreateRunInput, } from "@internal/run-store"; -import { RunId, ownerEngine, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { RunId, ownerEngine, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction } from "@trigger.dev/database"; import { expect } from "vitest"; import { RunEngine } from "../index.js"; @@ -101,7 +101,7 @@ function freshRunId() { } function freshKsuidRunId() { - return RunId.toFriendlyId(generateKsuidId()); + return RunId.toFriendlyId(generateRunOpsId()); } const baseTriggerParams = (friendlyId: string, environment: any, taskIdentifier: string) => ({ diff --git a/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts b/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts index ec6ec5cd3ec..4c2e1ea7ad6 100644 --- a/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts @@ -23,9 +23,9 @@ import type { RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH (runOpsResidency.ts): 25 chars → cuid → LEGACY, -// 27 chars → ksuid → NEW. +// a v1 body (26 chars, version "1" at index 25) → NEW. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / prisma14, full schema) -const KSUID_27 = "k".repeat(27); // → NEW (#new / prisma17, dedicated subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / prisma17, dedicated subset schema) async function seedEnvironment( prisma: AnyClient, @@ -100,7 +100,7 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchprobe_new"); - const batchId = `batch_${KSUID_27}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // ksuid → #new // Seed the batch directly on #new (5434), exactly where a runEngine-routed ksuid batch lives. await prisma17.batchTaskRun.create({ @@ -155,7 +155,7 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchfid_new"); - const batchId = `batch_${KSUID_27}`; + const batchId = `batch_${NEW_ID_26}`; const friendlyId = "batch_fid_new"; await prisma17.batchTaskRun.create({ @@ -184,7 +184,7 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchidem_new"); - const batchId = `batch_${KSUID_27}`; + const batchId = `batch_${NEW_ID_26}`; const idempotencyKey = "idem_batch_new"; await prisma17.batchTaskRun.create({ @@ -215,8 +215,8 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "attempt_new"); - const runId = `run_${KSUID_27}`; // ksuid run → #new - const attemptId = `attempt_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; // ksuid run → #new + const attemptId = `attempt_${NEW_ID_26}`; // The attempt's owning run lives on #new (the FK is co-resident on the dedicated schema). await prisma17.taskRun.create({ @@ -247,10 +247,10 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c number: 1, friendlyId: "attempt_fid_new", taskRunId: runId, - backgroundWorkerId: `bw_${KSUID_27}`, - backgroundWorkerTaskId: `bwt_${KSUID_27}`, + backgroundWorkerId: `bw_${NEW_ID_26}`, + backgroundWorkerTaskId: `bwt_${NEW_ID_26}`, runtimeEnvironmentId: env.environment.id, - queueId: `queue_${KSUID_27}`, + queueId: `queue_${NEW_ID_26}`, status: "PENDING", }, }); @@ -274,7 +274,7 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c // Single-DB config: both slots point at the same dedicated store (split effectively OFF). const router = new RoutingRunStore({ new: newStore, legacy: newStore }); const env = await seedEnvironment(prisma17, "dedicated", "splitoff_new"); - const batchId = `batch_${KSUID_27}`; + const batchId = `batch_${NEW_ID_26}`; await prisma17.batchTaskRun.create({ data: { diff --git a/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts b/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts index 7dfffb4ee6c..d1a4c5e9ba3 100644 --- a/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts @@ -9,8 +9,8 @@ import type { PrismaClient } from "@trigger.dev/database"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import { describe, expect } from "vitest"; -// 27-char internal id → ksuid → NEW (lives only on the dedicated run-ops DB). -const KSUID_27 = "k".repeat(27); +// v1 internal id (26 chars, version "1" at index 25) → NEW (lives only on the dedicated run-ops DB). +const NEW_ID_26 = "k".repeat(24) + "01"; async function seedControlPlaneAlertPrereqs(prisma: PrismaClient, suffix: string) { const organization = await prisma.organization.create({ @@ -61,7 +61,7 @@ describe("ProjectAlert control-plane → run-subgraph FK reconciliation", () => // The run exists ONLY on the dedicated run-ops DB (prisma17), never on control-plane. await (prisma17 as RunOpsPrismaClient).taskRun.create({ data: { - id: KSUID_27, + id: NEW_ID_26, friendlyId: `run_${suffix}`, engine: "V2", status: "COMPLETED_WITH_ERRORS", @@ -78,7 +78,7 @@ describe("ProjectAlert control-plane → run-subgraph FK reconciliation", () => }, }); - // Control-plane has no TaskRun row for KSUID_27. With the FK present this throws P2003; + // Control-plane has no TaskRun row for NEW_ID_26. With the FK present this throws P2003; // after the FK is dropped + the @relation removed it succeeds. const alert = await prisma14.projectAlert.create({ data: { @@ -88,16 +88,16 @@ describe("ProjectAlert control-plane → run-subgraph FK reconciliation", () => environmentId: environment.id, status: "PENDING", type: "TASK_RUN", - taskRunId: KSUID_27, + taskRunId: NEW_ID_26, }, }); - expect(alert.taskRunId).toBe(KSUID_27); + expect(alert.taskRunId).toBe(NEW_ID_26); // The scalar round-trips and can be re-read off the control-plane row (the read path resolves // the actual run via runStore.findRun against the run-ops DB). const reread = await prisma14.projectAlert.findUniqueOrThrow({ where: { id: alert.id } }); - expect(reread.taskRunId).toBe(KSUID_27); + expect(reread.taskRunId).toBe(NEW_ID_26); }, 120_000 ); @@ -113,7 +113,7 @@ describe("ProjectAlert control-plane → run-subgraph FK reconciliation", () => // A ksuid attempt id with no matching control-plane TaskRunAttempt row. With the FK present // this throws P2003; after the FK is dropped it succeeds. - const attemptId = "a".repeat(27); + const attemptId = "a".repeat(24) + "01"; const alert = await prisma14.projectAlert.create({ data: { friendlyId: `alert_${suffix}`, diff --git a/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts b/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts index e0cc3e217b4..9eb310d4118 100644 --- a/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts @@ -22,9 +22,9 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH (runOpsResidency.ts): 25 chars → cuid → LEGACY, -// 27 chars → ksuid → NEW. A `run_`-prefixed friendly id strips the first underscore before length. +// a v1 body (version "1" at index 25) → ksuid → NEW. A `run_`-prefixed friendly id strips the first underscore first. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / control-plane DB, full schema) -const KSUID_27 = "k".repeat(27); // → NEW (#new / dedicated run-ops DB, subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / dedicated run-ops DB, subset schema) // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (the run-ops // rows carry FK-free scalar ids), so we mint synthetic owning ids. On legacy we seed the real rows @@ -158,7 +158,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const store = makeDedicatedStore(prisma17); const rows = await store.findManyTaskRunWaitpoints({ - where: { taskRunId: `run_${KSUID_27}` }, + where: { taskRunId: `run_${NEW_ID_26}` }, select: { id: true, batchId: true, @@ -179,8 +179,8 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma17 }) => { const store = makeDedicatedStore(prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap4hyd_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -217,7 +217,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const store = makeLegacyStore(prisma14); const rows = await store.findManyTaskRunWaitpoints({ - where: { taskRunId: `run_${KSUID_27}` }, + where: { taskRunId: `run_${NEW_ID_26}` }, select: { id: true, waitpoint: { select: { id: true, status: true } }, @@ -236,7 +236,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const rows = await router.findManyTaskRunWaitpoints({ - where: { taskRunId: `run_${KSUID_27}` }, + where: { taskRunId: `run_${NEW_ID_26}` }, select: { id: true, waitpoint: { select: { id: true, status: true, type: true, completedAfter: true } }, @@ -261,8 +261,8 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cores_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -306,7 +306,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "xdb_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "xdb_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; // cuid → lives on #legacy // The completing token lives on #legacy (cuid MANUAL token blocking a ksuid run). @@ -361,8 +361,8 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "phantom_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${"p".repeat(27)}`; // ksuid-shaped, but never created anywhere + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${"p".repeat(24) + "01"}`; // ksuid-shaped, but never created anywhere await prisma17.taskRunWaitpoint.create({ data: { taskRunId: runId, waitpointId, projectId: newEnv.project.id }, @@ -477,7 +477,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap2k_new"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -491,7 +491,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const checkpoint = await router.createTaskRunCheckpoint( { data: { - friendlyId: `checkpoint_${KSUID_27}`, + friendlyId: `checkpoint_${NEW_ID_26}`, type: "DOCKER", location: "s3://bucket/ksuid-run-checkpoint", projectId: env.project.id, @@ -645,7 +645,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap5c_new"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -698,8 +698,8 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { // A ksuid parent run + its associated waitpoint live on #new (prisma17 / dedicated). const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3_new"); - const parentRunId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; + const parentRunId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; await prisma17.taskRun.create({ data: { id: parentRunId, @@ -839,7 +839,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap6_new"); - const runId = `run_${KSUID_27}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // ksuid run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid waitpoint id → would route to #legacy by id-shape await router.createRun( @@ -887,7 +887,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma14, "legacy", "gap6c_leg"); const runId = `run_${CUID_25}`; // cuid run → #legacy - const waitpointId = `waitpoint_${KSUID_27}`; // ksuid waitpoint id → would route to #new by id-shape + const waitpointId = `waitpoint_${NEW_ID_26}`; // ksuid waitpoint id → would route to #new by id-shape await router.createRun( buildCreateRunInput({ @@ -936,7 +936,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "cg1_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "cg1_leg"); - const runId = `run_${KSUID_27}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // ksuid run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid token → completed on #legacy await router.createRun( @@ -1013,7 +1013,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3b_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap3b_leg"); - const runId = `run_${KSUID_27}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // ksuid run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid standalone token → resides on #legacy // The ksuid run lives on #new. @@ -1084,7 +1084,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // Single-store cross-check: the #new store ALSO writes the edge directly (proving the fix is in // the store writer, not only the router routing). - const runId2 = `run_${"m".repeat(27)}`; // a second ksuid run on #new + const runId2 = `run_${"m".repeat(24) + "01"}`; // a second ksuid run on #new await prisma17.taskRun.create({ data: { id: runId2, @@ -1124,8 +1124,8 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap3bco_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; // ksuid token → co-resident on #new + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; // ksuid token → co-resident on #new await prisma17.taskRun.create({ data: { @@ -1185,7 +1185,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3bidem_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap3bidem_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; // cuid token → #legacy await prisma17.taskRun.create({ @@ -1251,8 +1251,8 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cg1c_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; // co-resident on #new + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; // co-resident on #new await router.createRun( buildCreateRunInput({ @@ -1317,7 +1317,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, legacyStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap13bt_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap13bt_leg"); - const runId = `run_${KSUID_27}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // ksuid run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid token → #legacy await router.createRun( @@ -1387,7 +1387,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, legacyStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap13cr_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap13cr_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; await router.createRun( @@ -1446,7 +1446,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, legacyStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap13cs_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap13cs_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; await router.createRun( @@ -1510,8 +1510,8 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap13ctl_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; // co-resident on #new + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; // co-resident on #new await router.createRun( buildCreateRunInput({ diff --git a/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts b/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts index 2d43fc90830..a058a939c0a 100644 --- a/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts @@ -26,9 +26,9 @@ import type { CreateRunInput, RunStore, RunStoreSchemaVariant } from "./types.js type AnyClient = PrismaClient | RunOpsPrismaClient; -// ownerEngine classifies by internal-id LENGTH: 25 chars → cuid → LEGACY, 27 chars → ksuid → NEW. +// ownerEngine classifies by the version char: no marker → cuid → LEGACY, v1 body → ksuid → NEW. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / control-plane DB, full schema) -const KSUID_27 = "k".repeat(27); // → NEW (#new / dedicated run-ops DB, subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / dedicated run-ops DB, subset schema) async function seedEnvironment( prisma: AnyClient, @@ -143,7 +143,7 @@ async function seedKsuidRun( suffix: string ): Promise<{ runId: string; env: { project: { id: string }; environment: { id: string } } }> { const env = await seedEnvironment(prisma17, "dedicated", suffix); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, diff --git a/internal-packages/run-store/src/batchCompletionResidency.test.ts b/internal-packages/run-store/src/batchCompletionResidency.test.ts index dc9730b74d3..9f2ccd12fa5 100644 --- a/internal-packages/run-store/src/batchCompletionResidency.test.ts +++ b/internal-packages/run-store/src/batchCompletionResidency.test.ts @@ -17,9 +17,9 @@ import type { RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH (runOpsResidency.ts): 25 chars → cuid → LEGACY, -// 27 chars → ksuid → NEW. +// a v1 body (26 chars, version "1" at index 25) → NEW. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / prisma14, full schema) -const KSUID_27 = "k".repeat(27); // → NEW (#new / prisma17, dedicated subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / prisma17, dedicated subset schema) async function seedEnvironment( prisma: AnyClient, @@ -127,7 +127,7 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "updbatch_new"); - const batchId = `batch_${KSUID_27}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // ksuid → #new await prisma17.batchTaskRun.create({ data: { @@ -193,7 +193,7 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "crbatch_new"); - const batchId = `batch_${KSUID_27}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // ksuid → #new const created = await router.createBatchTaskRun( { @@ -250,7 +250,7 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cntitems_new"); - const batchId = `batch_${KSUID_27}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // ksuid → #new await prisma17.batchTaskRun.create({ data: { @@ -261,8 +261,8 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont status: "PROCESSING", }, }); - const runA = `run_${KSUID_27.slice(0, -3)}cra`; - const runB = `run_${KSUID_27.slice(0, -3)}crb`; + const runA = `run_${NEW_ID_26.slice(0, -3)}ra1`; + const runB = `run_${NEW_ID_26.slice(0, -3)}rb1`; await seedDedicatedRun(prisma17, env.environment.id, runA); await seedDedicatedRun(prisma17, env.environment.id, runB); await prisma17.batchTaskRunItem.create({ diff --git a/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts b/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts index 7ee52506656..541a9e09401 100644 --- a/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts +++ b/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts @@ -24,7 +24,8 @@ type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id length (no internal underscore): 25 -> cuid -> LEGACY, // 27 -> ksuid -> NEW. const cuidLegacy = (seed: string) => (seed + "c".repeat(25)).slice(0, 25); -const ksuidNew = (seed: string) => (seed + "k".repeat(27)).slice(0, 27); +const ksuidNew = (seed: string) => + (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; async function seedEnvironment( prisma: AnyClient, diff --git a/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts b/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts index f8a4b6fc153..a943d407337 100644 --- a/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts +++ b/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts @@ -17,7 +17,7 @@ function fakeStore(slot: string, heldIds: Set): RunStore { } as unknown as RunStore; } -const KSUID_ID = "waitpoint_" + "a".repeat(27); +const KSUID_ID = "waitpoint_" + "a".repeat(24) + "01"; const CUID_ID = "waitpoint_" + "a".repeat(25); const UNCLASSIFIABLE_ID = "waitpoint_" + "a".repeat(26); diff --git a/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts b/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts index 8e523660b72..6cd1838397c 100644 --- a/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts +++ b/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts @@ -27,13 +27,13 @@ import type { type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH after stripping a single leading `_`: -// 25 chars → cuid → LEGACY, 27 chars → ksuid → NEW. So a classifiable id +// 25 chars → cuid → LEGACY, a v1 body (version "1" at index 25) → ksuid → NEW. So a classifiable id // must carry NO internal underscore. These mint a distinct id of the right length from a short seed. function cuidLegacy(seed: string): string { return (seed + "c".repeat(25)).slice(0, 25); // 25 chars, no underscore → LEGACY } function ksuidNew(seed: string): string { - return (seed + "k".repeat(27)).slice(0, 27); // 27 chars, no underscore → NEW + return (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; } // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (the run-ops @@ -248,7 +248,7 @@ describe("RoutingRunStore — cross-DB idempotency dedup probe", () => { async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cg2_b"); - const runId = ksuidNew("rbn"); // 27 chars → NEW home + const runId = ksuidNew("rbn"); // v1 body → NEW home const waitpointId = ksuidNew("wbn"); const idempotencyKey = "cg2-key-b"; const taskIdentifier = "my-task"; diff --git a/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts b/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts index f3b5d164655..2c13ce3fd2b 100644 --- a/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts +++ b/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts @@ -20,13 +20,13 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH after stripping a leading `_` -// (runOpsResidency.ts): 25 chars (no internal underscore) → cuid → LEGACY, 27 chars → ksuid → NEW. +// (runOpsResidency.ts): 25 chars (no internal underscore) → cuid → LEGACY, a v1 body (version "1" at index 25) → ksuid → NEW. // These mint a distinct classifiable id of the right length from a short seed. function cuidLegacy(seed: string): string { return (seed + "c".repeat(25)).slice(0, 25); // 25 chars → LEGACY (#legacy / prisma14) } function ksuidNew(seed: string): string { - return (seed + "k".repeat(27)).slice(0, 27); // 27 chars → NEW (#new / prisma17) + return (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; } // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (run-ops rows diff --git a/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts b/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts index a32ac9a12d5..4fdcfe75042 100644 --- a/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts +++ b/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts @@ -28,7 +28,7 @@ type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH: 25 chars → cuid → LEGACY, 27 → ksuid → NEW. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / prisma14, full schema) -const KSUID_27 = "k".repeat(27); // → NEW (#new / prisma17, dedicated subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / prisma17, dedicated subset schema) // A recording "replica" that has NOT yet caught up: its taskRun reads always come back empty and // record that they ran, so a replica-routed read misses the just-written row. Everything else @@ -224,7 +224,7 @@ describe("run-ops split — read-after-write reads the OWNING store's WRITER, no const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed = seedEnvironmentDedicated("raw_new"); - const runId = `run_${KSUID_27}`; // ksuid → NEW + const runId = `run_${NEW_ID_26}`; // ksuid → NEW await prisma17.taskRun.create({ data: taskRunData({ id: runId, diff --git a/internal-packages/run-store/src/runOpsStore.snapshots.test.ts b/internal-packages/run-store/src/runOpsStore.snapshots.test.ts index 564afa08131..0e482e0e874 100644 --- a/internal-packages/run-store/src/runOpsStore.snapshots.test.ts +++ b/internal-packages/run-store/src/runOpsStore.snapshots.test.ts @@ -60,9 +60,9 @@ async function seedEnvironment( return { organization, project, environment }; } -// ownerEngine classifies by internal-id LENGTH after stripping a single leading `_`: 27 chars +// ownerEngine classifies by the version char after stripping a single leading `_`: a v1 body // → ksuid → NEW (#new / dedicated run-ops DB subset), 25 chars → cuid → LEGACY (#legacy / full schema). -const KSUID_27 = "k".repeat(27); // → NEW residency, exercises the dedicated store +const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW residency, exercises the dedicated store const CUID_25 = "c".repeat(25); // → LEGACY residency, exercises the full-schema store function buildCreateRunInput(params: { @@ -201,7 +201,7 @@ describe("RunStore run-ops persistence — snapshots", () => { }; const legacyRunId = `run_${CUID_25}`; // → #legacy (full schema) - const newRunId = `run_${KSUID_27}`; // → #new (dedicated subset) + const newRunId = `run_${NEW_ID_26}`; // → #new (dedicated subset) const seed14 = await seed(prisma14, "legacy", legacyRunId, "sa14"); const seed17 = await seed(prisma17, "dedicated", newRunId, "sa17"); @@ -292,7 +292,7 @@ describe("RunStore run-ops persistence — snapshots", () => { }; const r14 = await run(prisma14, "legacy", `run_${CUID_25}`, "sb14"); - const r17 = await run(prisma17, "dedicated", `run_${KSUID_27}`, "sb17"); + const r17 = await run(prisma17, "dedicated", `run_${NEW_ID_26}`, "sb17"); // The join links the snapshot to both waitpoints (set-equal) on both stores. expect([...r14.joinIds].sort()).toEqual([r14.w1, r14.w2].sort()); @@ -344,7 +344,7 @@ describe("RunStore run-ops persistence — snapshots", () => { }; await seed(prisma14, "legacy", `run_${CUID_25}`, "sc14"); - await seed(prisma17, "dedicated", `run_${KSUID_27}`, "sc17"); + await seed(prisma17, "dedicated", `run_${NEW_ID_26}`, "sc17"); const orderedDescriptions = async (client: AnyClient) => { const rows = await (client as PrismaClient).$queryRawUnsafe<{ description: string }[]>( diff --git a/internal-packages/run-store/src/runOpsStore.test.ts b/internal-packages/run-store/src/runOpsStore.test.ts index d8a4a11c868..8ed194a1452 100644 --- a/internal-packages/run-store/src/runOpsStore.test.ts +++ b/internal-packages/run-store/src/runOpsStore.test.ts @@ -6,9 +6,9 @@ import { PostgresRunStore } from "./PostgresRunStore.js"; import { RoutingRunStore } from "./runOpsStore.js"; import type { CreateRunInput } from "./types.js"; -// 25-char internal id → cuid → LEGACY; 27-char internal id → ksuid → NEW. +// 25-char internal id → cuid → LEGACY; v1 internal id (26 chars, version "1" at index 25) → NEW. const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const NEW_ID_26 = "k".repeat(24) + "01"; async function seedEnvironment(prisma: PrismaClient, slugSuffix: string) { const organization = await prisma.organization.create({ @@ -229,7 +229,7 @@ describe("RoutingRunStore (TaskRun-core)", () => { const seed17 = await seedEnvironment(prisma17, "c17"); // (i) createRun lands on NEW, never on LEGACY. - const bornId = "run_born_on_new"; + const bornId = `run_${"b".repeat(24)}01`; await router.createRun( buildCreateRunInput({ runId: bornId, @@ -246,7 +246,7 @@ describe("RoutingRunStore (TaskRun-core)", () => { // (ii) seed a cuid-length (LEGACY) row on the legacy DB and a ksuid-length (NEW) row on // the new DB, then prove residency selection via ownerEngine length classification. const legacyRunId = `run_${CUID_25}`; - const newRunId = `run_${KSUID_27}`; + const newRunId = `run_${NEW_ID_26}`; await legacyStore.createRun( buildCreateRunInput({ runId: legacyRunId, @@ -357,7 +357,7 @@ describe("RoutingRunStore (TaskRun-core)", () => { // Use a ksuid-length (NEW-residency) id to exercise the route; in single-DB both // slots are the same store, so the round-trip must succeed on the one client. - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -484,7 +484,7 @@ describe("BatchTaskRun group", () => { const seed17 = await seedEnvironment(prisma17, "batchb17"); - const batchId = "batch_born_on_new"; + const batchId = `batch_${"b".repeat(24)}01`; await router.createBatchTaskRun( batchCreateData({ id: batchId, @@ -547,7 +547,7 @@ describe("BatchTaskRun group", () => { await newStore.createBatchTaskRun( batchCreateData({ - id: KSUID_27, + id: NEW_ID_26, friendlyId: "batch_ksuid_p8a", runtimeEnvironmentId: seed17.environment.id, runCount: 1, @@ -562,7 +562,7 @@ describe("BatchTaskRun group", () => { }) ); - expect((await router.findBatchTaskRunById(KSUID_27))?.id).toBe(KSUID_27); + expect((await router.findBatchTaskRunById(NEW_ID_26))?.id).toBe(NEW_ID_26); expect((await router.findBatchTaskRunById(CUID_25))?.id).toBe(CUID_25); } ); @@ -577,7 +577,7 @@ describe("BatchTaskRun group", () => { const seed14 = await seedEnvironment(prisma14, "p8a_upd14"); const seed17 = await seedEnvironment(prisma17, "p8a_upd17"); - const ksuidBatchId = `${KSUID_27.slice(0, -1)}u`; + const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}u1`; const cuidBatchId = `${CUID_25.slice(0, -1)}u`; await newStore.createBatchTaskRun( @@ -623,7 +623,7 @@ describe("BatchTaskRun group", () => { const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed17 = await seedEnvironment(prisma17, "p8a_inc17"); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}in`; + const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}i1`; await newStore.createBatchTaskRun( batchCreateData({ @@ -634,7 +634,7 @@ describe("BatchTaskRun group", () => { }) ); - const runId = `${KSUID_27.slice(0, -3)}run`; + const runId = `${NEW_ID_26.slice(0, -3)}ru1`; await prisma17.taskRun.create({ data: { id: runId, @@ -681,7 +681,7 @@ describe("BatchTaskRun group", () => { const seed14 = await seedEnvironment(prisma14, "p8c_cuid14"); const seed17 = await seedEnvironment(prisma17, "p8c_ksuid17"); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}c1`; + const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}c1`; const cuidBatchId = `${CUID_25.slice(0, -2)}c1`; await router.createBatchTaskRun( @@ -722,7 +722,7 @@ describe("BatchTaskRun group", () => { const seed14 = await seedEnvironment(prisma14, "p8c_probe14"); const seed17 = await seedEnvironment(prisma17, "p8c_probe17"); - const ksuidOnLegacy = `${KSUID_27.slice(0, -2)}pl`; + const ksuidOnLegacy = `${NEW_ID_26.slice(0, -2)}l1`; await legacyStore.createBatchTaskRun( batchCreateData({ id: ksuidOnLegacy, @@ -738,7 +738,7 @@ describe("BatchTaskRun group", () => { expect((await router.findBatchTaskRunById(ksuidOnLegacy))?.id).toBe(ksuidOnLegacy); - const ksuidOnNew = `${KSUID_27.slice(0, -2)}pn`; + const ksuidOnNew = `${NEW_ID_26.slice(0, -2)}n1`; await newStore.createBatchTaskRun( batchCreateData({ id: ksuidOnNew, @@ -766,7 +766,7 @@ describe("BatchTaskRun group", () => { const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); // The ksuid batch lives on NEW only — never on the control-plane DB. - const batchId = `${KSUID_27.slice(0, -2)}bw`; + const batchId = `${NEW_ID_26.slice(0, -2)}b1`; await prisma17.batchTaskRun.create({ data: { id: batchId, @@ -823,11 +823,11 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed17 = await seedEnvironment(prisma17, "txnew17"); - const newRunId = KSUID_27; + const newRunId = NEW_ID_26; await router.createRun( buildCreateRunInput({ runId: newRunId, - friendlyId: `run_${KSUID_27}`, + friendlyId: `run_${NEW_ID_26}`, taskIdentifier: "tx-task", organizationId: seed17.organization.id, projectId: seed17.project.id, @@ -854,7 +854,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () const seed17 = await seedEnvironment(prisma17, "fid17"); const legacyFriendly = `run_${CUID_25}`; - const newFriendly = `run_${KSUID_27}`; + const newFriendly = `run_${NEW_ID_26}`; await legacyStore.createRun( buildCreateRunInput({ runId: CUID_25, @@ -867,7 +867,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () ); await newStore.createRun( buildCreateRunInput({ - runId: KSUID_27, + runId: NEW_ID_26, friendlyId: newFriendly, taskIdentifier: "new-task", organizationId: seed17.organization.id, @@ -877,7 +877,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () ); expect((await router.findRun({ friendlyId: legacyFriendly }))?.id).toBe(CUID_25); - expect((await router.findRun({ friendlyId: newFriendly }))?.id).toBe(KSUID_27); + expect((await router.findRun({ friendlyId: newFriendly }))?.id).toBe(NEW_ID_26); } ); @@ -894,8 +894,8 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () await newStore.createRun( buildCreateRunInput({ - runId: KSUID_27, - friendlyId: `run_${KSUID_27}`, + runId: NEW_ID_26, + friendlyId: `run_${NEW_ID_26}`, taskIdentifier: "write-task", organizationId: seed17.organization.id, projectId: seed17.project.id, @@ -904,7 +904,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () ); const result = await router.updateMetadata( - KSUID_27, + NEW_ID_26, { metadata: '{"x":1}', metadataVersion: { increment: 1 }, updatedAt: new Date() }, {}, // Forwarded LEGACY client — must be ignored. @@ -913,7 +913,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () expect(result.count).toBe(1); const row = await prisma17.taskRun.findUnique({ - where: { id: KSUID_27 }, + where: { id: NEW_ID_26 }, select: { metadata: true }, }); expect(row?.metadata).toBe('{"x":1}'); @@ -923,9 +923,10 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { // Internal-id convention (matches the file): `run_` + a 25-char body (cuid → LEGACY) or - // a 27-char body (ksuid → NEW). The classifier strips `run_` then keys on body length. + // a v1 body (ksuid → NEW). The classifier strips `run_` then keys on the version char. const legacyId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; async function createRunOn( store: PostgresRunStore, @@ -1284,7 +1285,7 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { expect(await prisma14.waitpoint.findUnique({ where: { id: cuidWp } })).not.toBeNull(); expect(await prisma17.waitpoint.findUnique({ where: { id: cuidWp } })).toBeNull(); - const ksuidWp = `waitpoint_${KSUID_27}`; + const ksuidWp = `waitpoint_${NEW_ID_26}`; await router.createWaitpoint({ data: { id: ksuidWp, @@ -1306,7 +1307,8 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { // prisma17 is RunOpsPrismaClient (subset schema, no control-plane tables). describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () => { const legacyId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; heteroRunOpsPostgresTest( "id-set fans out across NEW (RunOpsPrismaClient) and LEGACY (PrismaClient) distinct schemas", @@ -1374,7 +1376,8 @@ describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () = describe("RoutingRunStore write-path fan-outs", () => { const legacyId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; async function createRunWithKey( store: PostgresRunStore, @@ -1592,7 +1595,8 @@ describe("RoutingRunStore write-path fan-outs", () => { describe("RoutingRunStore.findTaskRunAttempt residency routing", () => { const legacyRunId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newRunId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newRunId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; async function seedAttempt( prisma: PrismaClient, @@ -1930,7 +1934,7 @@ describe("RoutingRunStore batch-residency accessors", () => { // ksuid batch with an idempotency key on NEW (dedicated, scalar env id) await newStore.createBatchTaskRun( batchData({ - id: `${KSUID_27.slice(0, -2)}i1`, + id: `${NEW_ID_26.slice(0, -2)}i1`, friendlyId: "batch_idem_new", runtimeEnvironmentId: ENV_NEW, idempotencyKey: "key-new", @@ -1968,7 +1972,7 @@ describe("RoutingRunStore batch-residency accessors", () => { const seed14 = await seedEnvironment(prisma14, "optA_um14"); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}m1`; + const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}m1`; const cuidBatchId = `${CUID_25.slice(0, -2)}m1`; await newStore.createBatchTaskRun( batchData({ id: ksuidBatchId, friendlyId: "batch_um_new", runtimeEnvironmentId: ENV_NEW }) @@ -2018,13 +2022,13 @@ describe("RoutingRunStore batch-residency accessors", () => { const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}c1`; + const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}c1`; await newStore.createBatchTaskRun( batchData({ id: ksuidBatchId, friendlyId: "batch_cnt_new", runtimeEnvironmentId: ENV_NEW }) ); - const runA = `${KSUID_27.slice(0, -3)}cra`; - const runB = `${KSUID_27.slice(0, -3)}crb`; + const runA = `${NEW_ID_26.slice(0, -3)}ra1`; + const runB = `${NEW_ID_26.slice(0, -3)}rb1`; await seedDedicatedRun(prisma17, ENV_NEW, runA); await seedDedicatedRun(prisma17, ENV_NEW, runB); await prisma17.batchTaskRunItem.create({ @@ -2049,12 +2053,12 @@ describe("RoutingRunStore batch-residency accessors", () => { const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}u1`; + const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}u1`; await newStore.createBatchTaskRun( batchData({ id: ksuidBatchId, friendlyId: "batch_ui_new", runtimeEnvironmentId: ENV_NEW }) ); - const runX = `${KSUID_27.slice(0, -3)}uix`; + const runX = `${NEW_ID_26.slice(0, -3)}ux1`; await seedDedicatedRun(prisma17, ENV_NEW, runX); await prisma17.batchTaskRunItem.create({ data: { batchTaskRunId: ksuidBatchId, taskRunId: runX, status: "PENDING" }, @@ -2079,7 +2083,7 @@ describe("RoutingRunStore batch-residency accessors", () => { const store = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: store, legacy: store }); - const batchId = `${KSUID_27.slice(0, -2)}s1`; + const batchId = `${NEW_ID_26.slice(0, -2)}s1`; await router.createBatchTaskRun( batchData({ id: batchId, @@ -2093,7 +2097,7 @@ describe("RoutingRunStore batch-residency accessors", () => { batchId ); - const runId = `${KSUID_27.slice(0, -3)}srn`; + const runId = `${NEW_ID_26.slice(0, -3)}sr1`; await seedDedicatedRun(prisma17, ENV_NEW, runId); await prisma17.batchTaskRunItem.create({ data: { batchTaskRunId: batchId, taskRunId: runId, status: "PENDING" }, diff --git a/internal-packages/run-store/src/runOpsStore.ts b/internal-packages/run-store/src/runOpsStore.ts index 66bd4a976dd..f218f1d6ba0 100644 --- a/internal-packages/run-store/src/runOpsStore.ts +++ b/internal-packages/run-store/src/runOpsStore.ts @@ -69,8 +69,9 @@ export class RoutingRunStore implements RunStore { return this.#classify(id) === "NEW" ? this.#new : this.#legacy; } - // Best-effort route; falls back to NEW (the steady-state home) when the id is absent - // or unclassifiable. + // Best-effort route; falls back to NEW (the steady-state home) when the id is absent. + // Classification is total (any id without the v1 version marker is LEGACY), so the + // catch below only guards injected classifiers that still throw. #routeOrNew(id: string | undefined): RunStore { if (typeof id !== "string") { return this.#new; diff --git a/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts b/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts index 9017d8c85b0..25eb894a88e 100644 --- a/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts +++ b/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts @@ -19,9 +19,9 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; -// ownerEngine classifies by internal-id LENGTH after stripping a single leading `_`: 27 chars +// ownerEngine classifies by the version char after stripping a single leading `_`: a v1 body // → ksuid → NEW (#new / dedicated subset), 25 chars → cuid → LEGACY (#legacy / full schema). -const KSUID_27 = "k".repeat(27); +const NEW_ID_26 = "k".repeat(24) + "01"; const CUID_25 = "c".repeat(25); // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (the run-ops @@ -234,7 +234,7 @@ describe("RunStore run-ops persistence — waitpoints", () => { }; const wp14 = await run(prisma14, "legacy", `run_${CUID_25}`, "wa14"); - const wp17 = await run(prisma17, "dedicated", `run_${KSUID_27}`, "wa17"); + const wp17 = await run(prisma17, "dedicated", `run_${NEW_ID_26}`, "wa17"); expect(wp14).not.toBeNull(); expect(wp17).not.toBeNull(); @@ -324,7 +324,7 @@ describe("RunStore run-ops persistence — waitpoints", () => { { prisma: prisma17, schemaVariant: "dedicated" as const, - runId: `run_${KSUID_27}`, + runId: `run_${NEW_ID_26}`, suffix: "wb17", }, ]) { @@ -439,9 +439,9 @@ describe("RunStore run-ops persistence — waitpoints", () => { const env = await seedEnvironment(prisma14, "legacy", "wd14"); - // KSUID_27-length id → NEW residency, exercising the route; both slots are the same store so + // NEW_ID_26-length id → NEW residency, exercising the route; both slots are the same store so // it still lands on prisma14. - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -505,7 +505,7 @@ describe("RunStore run-ops persistence — waitpoints", () => { // The NEW run + its (synthetic) env live on the dedicated #new subset (prisma17). const env17 = await seedEnvironment(prisma17, "dedicated", "we17"); - const runId = `run_${KSUID_27}`; // ksuid → NEW residency + const runId = `run_${NEW_ID_26}`; // ksuid → NEW residency await router.createRun( buildCreateRunInput({ runId, @@ -568,7 +568,7 @@ describe("RunStore run-ops persistence — waitpoints", () => { const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const env17 = await seedEnvironment(prisma17, "dedicated", "wf17"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, diff --git a/packages/core/src/v3/isomorphic/friendlyId.test.ts b/packages/core/src/v3/isomorphic/friendlyId.test.ts index 9885cd14b81..439de1e8c86 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.test.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.test.ts @@ -4,15 +4,19 @@ import { WaitpointId, SnapshotId, QueueId, - generateKsuidId, - decodeKsuid, - KSUID_PAYLOAD_BYTES, + RUN_OPS_ID_LENGTH, + RUN_OPS_ID_REGION_INDEX, + RUN_OPS_ID_VERSION, + RUN_OPS_ID_VERSION_INDEX, + base32hexDecode, + base32hexEncode, + generateRunOpsId, + parseRunId, } from "./friendlyId.js"; const CUID_LEN = 25; -const KSUID_LEN = 27; -describe("RunId + WaitpointId mint cuid by default; ksuid via generateKsuidId", () => { +describe("RunId + WaitpointId mint cuid by default; run-ops v1 via generateRunOpsId", () => { it("default: run + waitpoint mint cuid (25) and round-trip", () => { for (const util of [RunId, WaitpointId]) { const { id, friendlyId } = util.generate(); @@ -24,11 +28,11 @@ describe("RunId + WaitpointId mint cuid by default; ksuid via generateKsuidId", } }); - it("explicit ksuid: a run/waitpoint friendlyId over generateKsuidId() is 27-char and round-trips", () => { + it("explicit run-ops id: a run/waitpoint friendlyId over generateRunOpsId() is 26-char and round-trips", () => { for (const util of [RunId, WaitpointId]) { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = util.toFriendlyId(id); - expect(id.length).toBe(KSUID_LEN); + expect(id.length).toBe(RUN_OPS_ID_LENGTH); expect(util.fromFriendlyId(friendlyId)).toBe(id); expect(util.toId(friendlyId)).toBe(id); expect(util.toId(id)).toBe(id); @@ -39,80 +43,161 @@ describe("RunId + WaitpointId mint cuid by default; ksuid via generateKsuidId", expect(SnapshotId.generate().id.length).toBe(CUID_LEN); expect(QueueId.generate().id.length).toBe(CUID_LEN); }); +}); - it("disjoint lengths: 27 (ksuid) vs 25 (cuid) — the classifier margin", () => { - expect(generateKsuidId().length).not.toBe(SnapshotId.generate().id.length); +describe("base32hex codec (lowercase RFC 4648 §7)", () => { + // Independent reference: interpret the bytes as one big-endian integer and + // emit base-32 digits. Only exact multiples of 5 bytes (40 bits) are used, so + // there is never a partial trailing group to disagree on. + const ALPHA = "0123456789abcdefghijklmnopqrstuv"; + function referenceEncode(bytes: Uint8Array): string { + let n = 0n; + for (const b of bytes) n = (n << 8n) | BigInt(b); + const chars = (bytes.length * 8) / 5; + let out = ""; + for (let i = 0; i < chars; i++) { + out = ALPHA[Number(n & 31n)] + out; + n >>= 5n; + } + return out; + } + + it("matches the big-integer reference bit-for-bit (property, 5/10/15/20-byte inputs)", () => { + for (let iter = 0; iter < 2_000; iter++) { + for (const len of [5, 10, 15, 20]) { + const bytes = new Uint8Array(len); + crypto.getRandomValues(bytes); + const encoded = base32hexEncode(bytes); + expect(encoded).toBe(referenceEncode(bytes)); + expect(Array.from(base32hexDecode(encoded))).toEqual(Array.from(bytes)); + } + } }); - it("generateKsuidId() is directly callable and yields 27 chars", () => { - expect(generateKsuidId().length).toBe(KSUID_LEN); + it("hand-verified vectors", () => { + expect(base32hexEncode(new Uint8Array(5))).toBe("00000000"); + expect(base32hexEncode(new Uint8Array(5).fill(0xff))).toBe("vvvvvvvv"); + expect(base32hexEncode(new Uint8Array([0, 0, 0, 0, 1]))).toBe("00000001"); + // 0x20 0 0 0 0 = 2^37; 2^37 / 32^7 = 4 → leading digit "4" + expect(base32hexEncode(new Uint8Array([0x20, 0, 0, 0, 0]))).toBe("40000000"); + }); + + it("decode rejects characters outside the lowercase base32hex alphabet", () => { + for (const bad of ["w", "x", "z", "A", "V", "-", "_", " "]) { + expect(() => base32hexDecode(`0000000${bad}`)).toThrow(/invalid/i); + } }); }); -describe("generateKsuidId is a genuine KSUID (decodable timestamp, time-ordered)", () => { +describe("generateRunOpsId — run-ops v1 id spec", () => { afterEach(() => vi.useRealTimers()); - it("is exactly 27 base62 chars", () => { - expect(generateKsuidId()).toMatch(/^[0-9A-Za-z]{27}$/); + it("emits <24-char base32hex core> — 26 chars total", () => { + const id = generateRunOpsId(); + expect(id.length).toBe(RUN_OPS_ID_LENGTH); + expect(id).toMatch(/^[0-9a-v]{24}[a-z0-9]1$/); + expect(id[RUN_OPS_ID_VERSION_INDEX]).toBe(RUN_OPS_ID_VERSION); + }); + + it("only ever uses lowercase [a-z0-9] and NEVER '-' (DNS-1123 / pod-name invariant)", () => { + for (let i = 0; i < 5_000; i++) { + const id = generateRunOpsId(); + expect(id).toMatch(/^[a-z0-9]+$/); + expect(id).not.toContain("-"); + } }); - it("carries a decodable timestamp within a few seconds of now", () => { - const before = Math.floor(Date.now() / 1000); - const { timestampSeconds: ts } = decodeKsuid(generateKsuidId()); - expect(ts).toBeGreaterThanOrEqual(before - 2); - expect(ts).toBeLessThanOrEqual(Math.floor(Date.now() / 1000) + 2); + it("stamps the region char from REGION_CODES, defaulting to '0' when unknown/absent", () => { + expect(generateRunOpsId("us-east-1")[RUN_OPS_ID_REGION_INDEX]).toBe("e"); + expect(generateRunOpsId("us-west-2")[RUN_OPS_ID_REGION_INDEX]).toBe("w"); + expect(generateRunOpsId("eu-central-1")[RUN_OPS_ID_REGION_INDEX]).toBe("c"); + expect(generateRunOpsId("mars-north-1")[RUN_OPS_ID_REGION_INDEX]).toBe("0"); + expect(generateRunOpsId()[RUN_OPS_ID_REGION_INDEX]).toBe("0"); }); - it("is k-sortable: ids from later seconds sort lexicographically after earlier ones", () => { + it("sorts lexicographically in creation order at ms resolution (A@t, C@t+3ms, B@t+1s → A,C,B)", () => { vi.useFakeTimers(); - const ids: string[] = []; - for (const t of ["2026-01-01T00:00:00Z", "2026-01-01T00:05:00Z", "2026-09-01T12:00:00Z"]) { - vi.setSystemTime(new Date(t)); - ids.push(generateKsuidId()); + const t = new Date("2026-07-04T12:00:00.000Z").getTime(); + vi.setSystemTime(t); + const a = generateRunOpsId(); + vi.setSystemTime(t + 1000); + const b = generateRunOpsId(); + vi.setSystemTime(t + 3); + const c = generateRunOpsId(); + expect([b, c, a].sort()).toEqual([a, c, b]); + }); + + it("decode recovers the exact ms timestamp", () => { + vi.useFakeTimers(); + const t = new Date("2026-07-04T12:34:56.789Z"); + vi.setSystemTime(t); + const parsed = parseRunId(`run_${generateRunOpsId("us-east-1")}`); + expect(parsed.format).toBe("b32hex"); + if (parsed.format === "b32hex") { + expect(parsed.timestamp.getTime()).toBe(t.getTime()); } - expect([...ids].sort()).toEqual(ids); }); - it("is unique across many mints in the same second", () => { - const n = 1000; - expect(new Set(Array.from({ length: n }, () => generateKsuidId())).size).toBe(n); + it("is unique across many mints in the same ms (72 bits of CSPRNG)", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-04T00:00:00.000Z")); + const n = 2_000; + expect(new Set(Array.from({ length: n }, () => generateRunOpsId())).size).toBe(n); }); }); -describe("KSUID payload encode/decode (foundation primitive)", () => { - it("round-trips a full 16-byte payload exactly", () => { - const payload = new Uint8Array(KSUID_PAYLOAD_BYTES).map((_, i) => (i * 17 + 1) & 0xff); - const { payload: decoded } = decodeKsuid(generateKsuidId(payload)); - expect(Array.from(decoded)).toEqual(Array.from(payload)); +describe("parseRunId — version-char discrimination (not length)", () => { + it("parses a v1 friendly id as partitioned with region + version", () => { + const parsed = parseRunId(`run_${generateRunOpsId("us-west-2")}`); + expect(parsed).toMatchObject({ + format: "b32hex", + table: "partitioned", + region: "w", + version: "1", + }); }); - it("preserves a partial payload prefix and keeps the remainder for entropy", () => { - const meta = new Uint8Array([9, 8, 7, 6]); - const { payload } = decodeKsuid(generateKsuidId(meta)); - expect(Array.from(payload.slice(0, 4))).toEqual([9, 8, 7, 6]); - expect(payload.length).toBe(KSUID_PAYLOAD_BYTES); + it("classifies a cuid friendly id legacy", () => { + expect(parseRunId(RunId.generate().friendlyId)).toEqual({ + format: "legacy", + table: "legacy", + }); }); - it("still carries a decodable timestamp when a payload is embedded", () => { - const before = Math.floor(Date.now() / 1000); - const { timestampSeconds } = decodeKsuid(generateKsuidId(new Uint8Array([1, 2, 3]))); - expect(timestampSeconds).toBeGreaterThanOrEqual(before - 2); - expect(timestampSeconds).toBeLessThanOrEqual(Math.floor(Date.now() / 1000) + 2); + it("classifies a nanoid-bodied friendly id and a run_-less id legacy", () => { + expect(parseRunId("run_123456789abcdefghijkm").format).toBe("legacy"); // 21-char nanoid body + expect(parseRunId(generateRunOpsId()).format).toBe("legacy"); // bare body, no run_ prefix + expect(parseRunId("waitpoint_" + generateRunOpsId()).format).toBe("legacy"); // wrong prefix }); - it("stays 27 chars with a full payload and decodes through a friendlyId prefix", () => { - const id = generateKsuidId(new Uint8Array(KSUID_PAYLOAD_BYTES).fill(0xab)); - expect(id).toMatch(/^[0-9A-Za-z]{27}$/); - expect(Array.from(decodeKsuid(`run_${id}`).payload)).toEqual( - new Array(KSUID_PAYLOAD_BYTES).fill(0xab) - ); + it("falls back to legacy on a malformed v1 (bad alphabet / wrong version char)", () => { + expect(parseRunId(`run_${"A".repeat(25)}1`).format).toBe("legacy"); // uppercase core + expect(parseRunId(`run_${"a".repeat(25)}2`).format).toBe("legacy"); // wrong version + expect(parseRunId(`run_${"a".repeat(24)}-1`).format).toBe("legacy"); // region char not [a-z0-9] + expect(parseRunId(`run_${"a".repeat(27)}`).format).toBe("legacy"); // old 27-char shape }); +}); - it("throws if the payload exceeds the 16-byte budget", () => { - expect(() => generateKsuidId(new Uint8Array(KSUID_PAYLOAD_BYTES + 1))).toThrow(); +describe("firekeeper pod-name round-trip (runner-[-attempt-N] → run_)", () => { + // Mirrors firekeeper's runIDFromPodName: strip "runner-", cut before the first + // hyphen, prepend "run_". Works because a v1 id is all-lowercase [0-9a-v] and + // NEVER contains "-" — the hyphens all belong to the pod-name delimiters. + function firekeeperRunIdFromPodName(name: string): string { + const rest = name.slice("runner-".length); + const hyphen = rest.indexOf("-"); + return `run_${hyphen === -1 ? rest : rest.slice(0, hyphen)}`; + } + + it("recovers the exact id (incl. region + version chars) from first-attempt and retry pods", () => { + const id = generateRunOpsId("us-east-1"); + expect(firekeeperRunIdFromPodName(`runner-${id}`)).toBe(`run_${id}`); + expect(firekeeperRunIdFromPodName(`runner-${id}-attempt-2`)).toBe(`run_${id}`); + expect(parseRunId(firekeeperRunIdFromPodName(`runner-${id}-attempt-2`)).format).toBe("b32hex"); }); - it("decodeKsuid rejects a body that is not 27 base62 chars", () => { - expect(() => decodeKsuid("run_tooShort")).toThrow(); + it("the recovered id is a valid DNS-1123 label body (k8s accepts runner-)", () => { + const podName = `runner-${generateRunOpsId("eu-central-1")}`; + expect(podName).toMatch(/^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/); + expect(podName.length).toBeLessThanOrEqual(63); }); }); diff --git a/packages/core/src/v3/isomorphic/friendlyId.ts b/packages/core/src/v3/isomorphic/friendlyId.ts index bb0e8a2acc7..4a466dfd8ed 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.ts @@ -7,13 +7,42 @@ export function generateFriendlyId(prefix: string, size?: number) { return `${prefix}_${idGenerator(size)}`; } -// KSUID epoch (2014-05-13T16:53:20Z) — seconds offset applied to the unix timestamp. -const KSUID_EPOCH = 1_400_000_000; -const KSUID_TIMESTAMP_BYTES = 4; -export const KSUID_PAYLOAD_BYTES = 16; -const KSUID_TOTAL_BYTES = KSUID_TIMESTAMP_BYTES + KSUID_PAYLOAD_BYTES; -export const KSUID_STRING_LENGTH = 27; -const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +// Run-ops v1 id: `<24-char base32hex core>` — 26 chars. +// Core = 6-byte big-endian unix ms timestamp + 9 bytes CSPRNG. Invariants: +// - alphabet is lowercase [a-z0-9] (base32hex is [0-9a-v]): DNS-1123 safe for +// k8s pod names, and byte order == lexicographic order, so ids sort in mint +// order at ms resolution; +// - the id NEVER contains "-" — that delimiter belongs to pod-name suffixes +// (`runner--attempt-N`), so the id round-trips through a pod name by +// cutting at the first hyphen; +// - the `run_` (friendly) and `runner-` (pod) prefixes are part of the spec: +// they guarantee the k8s name starts with a letter even though a base32hex +// core can start with a digit. +const RUN_OPS_ID_ALPHABET = "0123456789abcdefghijklmnopqrstuv"; // base32hex, lowercase (RFC 4648 §7) +export const RUN_OPS_ID_LENGTH = 26; +export const RUN_OPS_ID_REGION_INDEX = 24; +export const RUN_OPS_ID_VERSION_INDEX = 25; +export const RUN_OPS_ID_VERSION = "1"; +const RUN_OPS_ID_CORE_BYTES = 15; // 6 timestamp + 9 random → exactly 24 base32hex chars +const RUN_OPS_ID_CORE_LENGTH = 24; +const RUN_OPS_ID_TIMESTAMP_BYTES = 6; + +/** Region char stamped when the region is unknown or unmapped at mint. */ +export const DEFAULT_REGION_CHAR = "0"; +// The region char is a raw positional char (readable via charAt before any +// decoding), NOT part of the base32hex core — so it may use the full DNS-safe +// lowercase [a-z0-9] range (e.g. "w" for us-west-2, which is outside [0-9a-v]). +const REGION_CHAR_PATTERN = /^[a-z0-9]$/; +/** One lowercase [a-z0-9] char per supported region, at RUN_OPS_ID_REGION_INDEX. */ +export const REGION_CODES: Readonly> = { + "us-east-1": "e", + "us-west-2": "w", + "eu-central-1": "c", +}; + +export function regionCharForRegion(region: string | undefined): string { + return (region && REGION_CODES[region]) || DEFAULT_REGION_CHAR; +} // globalThis.crypto is absent on Node 18.20 (a supported engine) without a flag, so fall back to // node:crypto's webcrypto, loaded only when the global is missing to stay isomorphic. @@ -40,116 +69,111 @@ function loadNodeWebCrypto(): Crypto | undefined { } // Resolve the crypto source lazily on first use (memoized), so merely importing this -// widely-used module never throws when crypto is unavailable — only minting a KSUID would. +// widely-used module never throws when crypto is unavailable — only minting an id would. let cachedGetRandomValues: RandomFiller | undefined; const getRandomValues: RandomFiller = (array) => (cachedGetRandomValues ??= resolveGetRandomValues())(array); -/** Encode raw bytes as base62 (big-endian), left-padded to the given length. */ -function base62Encode(bytes: Uint8Array, length: number): string { - const digits = Array.from(bytes); - let result = ""; - - while (digits.length > 0) { - let remainder = 0; - const quotient: number[] = []; - - for (let i = 0; i < digits.length; i++) { - const acc = (digits[i] ?? 0) + remainder * 256; - const q = Math.floor(acc / 62); - remainder = acc % 62; - - if (quotient.length > 0 || q > 0) { - quotient.push(q); - } +/** Lowercase base32hex (RFC 4648 §7): 5 bits per char, order-preserving, no padding. */ +export function base32hexEncode(bytes: Uint8Array): string { + let out = ""; + let buf = 0; + let bits = 0; + for (const b of bytes) { + buf = (buf << 8) | b; + bits += 8; + while (bits >= 5) { + out += RUN_OPS_ID_ALPHABET[(buf >> (bits - 5)) & 31]; + bits -= 5; } - - result = BASE62_ALPHABET.charAt(remainder) + result; - digits.length = 0; - digits.push(...quotient); } + return out; +} - return result.padStart(length, BASE62_ALPHABET.charAt(0)); +/** Inverse of base32hexEncode. Throws on characters outside the lowercase alphabet. */ +export function base32hexDecode(s: string): Uint8Array { + const out: number[] = []; + let buf = 0; + let bits = 0; + for (const c of s) { + const v = RUN_OPS_ID_ALPHABET.indexOf(c); + if (v === -1) { + throw new Error(`invalid run id char: ${c}`); + } + buf = (buf << 5) | v; + bits += 5; + if (bits >= 8) { + out.push((buf >> (bits - 8)) & 0xff); + bits -= 8; + } + } + return Uint8Array.from(out); } /** - * 27-char, base62, time-ordered KSUID body (length-disjoint from the 25-char cuid): a 4-byte - * timestamp (seconds since the KSUID epoch) + a 16-byte payload; ids from different seconds - * sort in mint order. Payload defaults to CSPRNG entropy; callers may supply up to - * KSUID_PAYLOAD_BYTES metadata bytes (written first, remainder stays random for uniqueness). + * Mint a run-ops v1 id body (26 chars, no prefix): 24-char base32hex core + * (6-byte ms timestamp + 9 CSPRNG bytes) + region char + version char "1". + * The trailing version char at RUN_OPS_ID_VERSION_INDEX is the residency + * discriminator — see runOpsResidency.ts. */ -export function generateKsuidId(payload?: Uint8Array): string { - const bytes = new Uint8Array(KSUID_TOTAL_BYTES); - - const timestamp = Math.floor(Date.now() / 1000) - KSUID_EPOCH; - bytes[0] = (timestamp >>> 24) & 0xff; - bytes[1] = (timestamp >>> 16) & 0xff; - bytes[2] = (timestamp >>> 8) & 0xff; - bytes[3] = timestamp & 0xff; - - if (payload && payload.length > KSUID_PAYLOAD_BYTES) { - throw new Error( - `KSUID payload must be at most ${KSUID_PAYLOAD_BYTES} bytes (got ${payload.length})` - ); - } - const reserved = payload?.length ?? 0; - if (payload && reserved > 0) { - bytes.set(payload, KSUID_TIMESTAMP_BYTES); - } - if (reserved < KSUID_PAYLOAD_BYTES) { - getRandomValues(bytes.subarray(KSUID_TIMESTAMP_BYTES + reserved)); +export function generateRunOpsId(region?: string): string { + const core = new Uint8Array(RUN_OPS_ID_CORE_BYTES); + + let ms = Date.now(); + for (let i = RUN_OPS_ID_TIMESTAMP_BYTES - 1; i >= 0; i--) { + core[i] = ms % 256; + ms = Math.floor(ms / 256); } + getRandomValues(core.subarray(RUN_OPS_ID_TIMESTAMP_BYTES)); - return base62Encode(bytes, KSUID_STRING_LENGTH); + return `${base32hexEncode(core)}${regionCharForRegion(region)}${RUN_OPS_ID_VERSION}`; } -/** Decoded parts of a KSUID body: its mint timestamp and 16-byte payload. */ -export type DecodedKsuid = { - timestampSeconds: number; - timestamp: Date; - payload: Uint8Array; -}; +export type ParsedRunId = + | { format: "b32hex"; table: "partitioned"; timestamp: Date; region: string; version: string } + | { format: "legacy"; table: "legacy" }; + +const LEGACY_RUN_ID: ParsedRunId = { format: "legacy", table: "legacy" }; /** - * Decode a KSUID body (or a `prefix_` friendly id) into its timestamp + 16-byte payload. - * The inverse of generateKsuidId's layout. Throws if the body is not 27 base62 chars. + * Parse a v1 id body (no prefix). Returns undefined unless the body is exactly + * 26 chars with version "1" at index 25 and every char inside the base32hex + * alphabet — anything else (cuid, nanoid, pre-cutover 27-char base62, malformed + * v1) is a legacy shape. */ -export function decodeKsuid(idOrFriendlyId: string): DecodedKsuid { - const underscore = idOrFriendlyId.indexOf("_"); - const body = underscore === -1 ? idOrFriendlyId : idOrFriendlyId.slice(underscore + 1); - if (body.length !== KSUID_STRING_LENGTH) { - throw new Error( - `Not a KSUID body: expected ${KSUID_STRING_LENGTH} base62 chars, got ${body.length}` - ); +export function parseRunOpsIdBody( + body: string +): { timestamp: Date; region: string; version: string } | undefined { + if (body.length !== RUN_OPS_ID_LENGTH) return undefined; + if (body[RUN_OPS_ID_VERSION_INDEX] !== RUN_OPS_ID_VERSION) return undefined; + const region = body[RUN_OPS_ID_REGION_INDEX] ?? ""; + if (!REGION_CHAR_PATTERN.test(region)) return undefined; + + let core: Uint8Array; + try { + core = base32hexDecode(body.slice(0, RUN_OPS_ID_CORE_LENGTH)); + } catch { + return undefined; } - let n = BigInt(0); - for (const ch of body) { - const digit = BASE62_ALPHABET.indexOf(ch); - if (digit < 0) { - throw new Error(`Invalid base62 character in KSUID body: ${ch}`); - } - n = n * BigInt(62) + BigInt(digit); + let ms = 0; + for (let i = 0; i < RUN_OPS_ID_TIMESTAMP_BYTES; i++) { + ms = ms * 256 + (core[i] ?? 0); } - const bytes = new Uint8Array(KSUID_TOTAL_BYTES); - for (let i = KSUID_TOTAL_BYTES - 1; i >= 0; i--) { - bytes[i] = Number(n & BigInt(0xff)); - n >>= BigInt(8); - } + return { timestamp: new Date(ms), region, version: RUN_OPS_ID_VERSION }; +} + +/** True if the (prefixless) id body is a well-formed run-ops v1 id. */ +export function isRunOpsIdBody(body: string): boolean { + return parseRunOpsIdBody(body) !== undefined; +} - const timestampSeconds = - (bytes[0] ?? 0) * 0x1000000 + - (bytes[1] ?? 0) * 0x10000 + - (bytes[2] ?? 0) * 0x100 + - (bytes[3] ?? 0) + - KSUID_EPOCH; - - return { - timestampSeconds, - timestamp: new Date(timestampSeconds * 1000), - payload: bytes.slice(KSUID_TIMESTAMP_BYTES), - }; +/** Parse a `run_`-prefixed friendly id; anything not a well-formed v1 id is legacy. */ +export function parseRunId(id: string): ParsedRunId { + if (!id.startsWith("run_")) return LEGACY_RUN_ID; + const parsed = parseRunOpsIdBody(id.slice(4)); + return parsed ? { format: "b32hex", table: "partitioned", ...parsed } : LEGACY_RUN_ID; } export function generateInternalId(): string { diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts index 9ed13b05beb..2a042494f51 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts @@ -1,17 +1,11 @@ import { describe, expect, it } from "vitest"; -import { RunId, WaitpointId, SnapshotId, generateKsuidId } from "./friendlyId.js"; -import { - ownerEngine, - classifyResidency, - classifyKind, - isClassifiable, - UnclassifiableRunId, -} from "./runOpsResidency.js"; +import { RunId, WaitpointId, BatchId, SnapshotId, generateRunOpsId } from "./friendlyId.js"; +import { ownerEngine, classifyResidency, classifyKind, isClassifiable } from "./runOpsResidency.js"; const SAMPLES = 50_000; // property-scale; CI-fast. (Bump locally toward "millions" for deeper coverage.) -describe("ownerEngine — residency classifier", () => { - it("cuid-length ids (default mint) classify LEGACY, friendly + internal", () => { +describe("ownerEngine — residency classifier (version char at fixed position, not length)", () => { + it("cuid ids (default mint) classify LEGACY, friendly + internal", () => { for (const util of [RunId, WaitpointId]) { const { id, friendlyId } = util.generate(); expect(ownerEngine(id)).toBe("LEGACY"); @@ -22,9 +16,9 @@ describe("ownerEngine — residency classifier", () => { } }); - it("ksuid-length ids (explicit generateKsuidId) classify NEW, friendly + internal", () => { - for (const util of [RunId, WaitpointId]) { - const id = generateKsuidId(); + it("run-ops v1 ids (generateRunOpsId) classify NEW, friendly + internal, across id-shape co-located entities", () => { + for (const util of [RunId, WaitpointId, BatchId]) { + const id = generateRunOpsId("us-east-1"); const friendlyId = util.toFriendlyId(id); expect(ownerEngine(id)).toBe("NEW"); expect(ownerEngine(friendlyId)).toBe("NEW"); @@ -33,27 +27,32 @@ describe("ownerEngine — residency classifier", () => { } }); - it("disjointness: no cuid sample is ever NEW, no ksuid sample is ever LEGACY", () => { - for (let i = 0; i < SAMPLES; i++) { - expect(ownerEngine(RunId.generate().id)).toBe("LEGACY"); - expect(ownerEngine(generateKsuidId())).toBe("NEW"); - } + it("discriminates on the version char, not length: 26+'1' → NEW, 26+'2' → LEGACY", () => { + const v1 = "a".repeat(24) + "e1"; + expect(ownerEngine(v1)).toBe("NEW"); + expect(ownerEngine("a".repeat(24) + "e2")).toBe("LEGACY"); + expect(ownerEngine("a".repeat(26))).toBe("LEGACY"); // 26 chars but no version marker }); - it("throws UnclassifiableRunId on malformed lengths (24, 26, 28, empty)", () => { - for (const bad of ["", "x".repeat(24), "x".repeat(26), "x".repeat(28), "x".repeat(40)]) { - expect(() => ownerEngine(bad)).toThrow(UnclassifiableRunId); - expect(isClassifiable(bad)).toBe(false); + it("malformed v1 shapes fall back to LEGACY (never throw)", () => { + for (const bad of [ + "", + "x".repeat(24) + "01", // 'x' outside base32hex + "A".repeat(25) + "1", // uppercase + "a".repeat(24) + "-1", // hyphen region char + "a".repeat(27), // pre-cutover 27-char shape → LEGACY under the version rule + "run_" + "b".repeat(27), // 27-char base62 KSUID-era friendly id → LEGACY + "x".repeat(40), + ]) { + expect(ownerEngine(bad)).toBe("LEGACY"); + expect(isClassifiable(bad)).toBe(true); // classification is total now } }); - it("error carries the offending value + length for diagnostics", () => { - try { - ownerEngine("x".repeat(26)); - throw new Error("should have thrown"); - } catch (e) { - expect(e).toBeInstanceOf(UnclassifiableRunId); - expect((e as UnclassifiableRunId).message).toContain("26"); + it("disjointness: no cuid sample is ever NEW, no v1 sample is ever LEGACY", () => { + for (let i = 0; i < SAMPLES; i++) { + expect(ownerEngine(RunId.generate().id)).toBe("LEGACY"); + expect(ownerEngine(generateRunOpsId())).toBe("NEW"); } }); diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.ts b/packages/core/src/v3/isomorphic/runOpsResidency.ts index edecec5ee7b..1aa4c335c93 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.ts @@ -1,26 +1,29 @@ -import { KSUID_STRING_LENGTH } from "./friendlyId.js"; +import { isRunOpsIdBody } from "./friendlyId.js"; /** The two run-ops stores a run/waitpoint can reside in. */ export type Residency = "LEGACY" | "NEW"; -/** Underlying id format. cuid → LEGACY store, ksuid → NEW store. */ +/** + * Underlying id lineage. "ksuid" is the historical label for the NEW-store + * mint path (now a base32hex run-ops v1 id — see friendlyId.ts); it is kept + * because the value is persisted in the runOpsMintKsuid feature flag. "cuid" + * is every legacy shape (cuid, nanoid, pre-cutover 27-char base62). + */ export type ResidencyKind = "cuid" | "ksuid"; -/** @bugsnag/cuid emits 25-char ids (cuid path, flag OFF). */ +/** @bugsnag/cuid emits 25-char ids (legacy mint path, flag OFF). */ export const CUID_LENGTH = 25; -/** KSUID / nanoid-27 emits 27-char ids (ksuid path, flag ON). */ -export const KSUID_LENGTH = KSUID_STRING_LENGTH; -/** Thrown when an id length matches neither the cuid nor the ksuid margin. */ +/** + * Kept for API compatibility: the default classifier no longer throws (every + * non-v1 shape is legacy), but injected classifiers may still raise it and + * callers still catch it. + */ export class UnclassifiableRunId extends Error { readonly value: string; readonly valueLength: number; constructor(value: string) { - super( - `Unclassifiable run-ops id: length ${value.length} matches neither cuid (${CUID_LENGTH}) nor ksuid (${KSUID_LENGTH}) — value=${JSON.stringify( - value - )}` - ); + super(`Unclassifiable run-ops id: value=${JSON.stringify(value)} (length ${value.length})`); this.name = "UnclassifiableRunId"; this.value = value; this.valueLength = value.length; @@ -38,21 +41,22 @@ function internalForm(id: string): string { return underscore === -1 ? id : id.slice(underscore + 1); } -/** Returns the underlying id FORMAT (cuid|ksuid), or throws if unclassifiable. */ +/** + * Returns the id lineage by the version-char rule: a well-formed run-ops v1 + * body (26 chars, version "1" at index 25, base32hex alphabet) is "ksuid" + * (NEW store); everything else — including malformed v1 shapes — is "cuid" + * (legacy). Total: never throws. + */ export function classifyKind(id: string): ResidencyKind { - const internal = internalForm(id); - if (internal.length === CUID_LENGTH) return "cuid"; - if (internal.length === KSUID_LENGTH) return "ksuid"; - throw new UnclassifiableRunId(id); + return isRunOpsIdBody(internalForm(id)) ? "ksuid" : "cuid"; } -/** Non-throwing predicate: is this id length one we can classify? */ -export function isClassifiable(id: string): boolean { - const len = internalForm(id).length; - return len === CUID_LENGTH || len === KSUID_LENGTH; +/** Classification is total now; kept for API compatibility. */ +export function isClassifiable(_id: string): boolean { + return true; } -/** Map an id to its owning run-ops store by length. Throws on ambiguity. */ +/** Map an id to its owning run-ops store by the version-char rule. */ export function classifyResidency(id: string): Residency { return classifyKind(id) === "ksuid" ? "NEW" : "LEGACY"; } From 2a92b7a5ca4dbebea51e18a9d76e55fc5db532e9 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 00:22:55 +0100 Subject: [PATCH 2/9] chore(run-ops): rename stale "ksuid" identifiers to runOpsId (no behavior change) The run-ops NEW-store run id is a 26-char base32hex id, not a KSUID, so code identifiers named "ksuid" were stale and misleading. Rename them to a consistent runOpsId/run-ops family (vars, test constants, helpers, fixture strings) and update comments/test descriptions to describe the current design. Also fixes 3 review findings in the touched test files: apiRunResultPresenter's newFriendlyId helper now mints a valid 26-char NEW body (was 27-char, silently LEGACY), lifecycleRouter's newRunId maps suffixes without the lossy outlier->"0" collision, and triggerTask uses the static generateRunOpsId import. Load-bearing wire values are untouched: the RUN_OPS_MINT_KSUID_ENABLED env var, the runOpsMintKsuid feature-flag key, and the persisted "ksuid"/"cuid" enum values. The one KSUID history note and the checksummed migration comment remain. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/webapp/app/env.server.ts | 4 +- .../v3/ApiBatchResultsPresenter.server.ts | 2 +- .../v3/ApiWaitpointPresenter.server.ts | 2 +- .../v3/BatchListPresenter.server.ts | 2 +- .../presenters/v3/BatchPresenter.server.ts | 2 +- ....runs.$runFriendlyId.input-streams.wait.ts | 2 +- ...uns.$runFriendlyId.session-streams.wait.ts | 2 +- ...ne.v1.runs.$runFriendlyId.wait.duration.ts | 2 +- .../idempotencyResidency.server.test.ts | 8 +- .../services/streamBatchItems.server.ts | 2 +- .../services/triggerFailedTask.server.ts | 2 +- .../runEngine/services/triggerTask.server.ts | 4 +- .../app/services/realtime/sessions.server.ts | 2 +- .../runsReplicationInstance.server.ts | 6 +- .../clickhouseRunsRepository.server.ts | 2 +- .../runsRepository/runsRepository.server.ts | 2 +- apps/webapp/app/v3/featureFlags.ts | 2 +- .../app/v3/runEngineHandlersShared.server.ts | 2 +- .../mintBatchFriendlyId.server.test.ts | 8 +- .../v3/runOpsMigration/readThrough.server.ts | 4 +- .../resolveInheritedMintKind.server.test.ts | 4 +- .../resolveInheritedMintKind.server.ts | 2 +- .../runOpsCascadeCleanup.server.ts | 2 +- .../runOpsMintKind.flipLatency.test.ts | 6 +- .../runOpsMintKind.server.test.ts | 4 +- .../v3/runOpsMigration/splitMode.server.ts | 2 +- apps/webapp/app/v3/runStore.server.test.ts | 10 +- .../app/v3/services/batchTriggerV3.server.ts | 8 +- .../v3/services/createCheckpoint.server.ts | 4 +- .../services/executeTasksWaitingForDeploy.ts | 2 +- .../test/SpanPresenter.readthrough.test.ts | 8 +- .../test/api.v1.waitpoints.tokens.test.ts | 14 +- ...apiBatchResultsPresenter.readroute.test.ts | 2 +- .../apiRunResultPresenter.readthrough.test.ts | 8 +- .../apiWaitpointPresenter.readthrough.test.ts | 4 +- ...batchTriggerV3ResidencyInheritance.test.ts | 20 +-- .../cancelDevSessionRunsStoreRouting.test.ts | 4 +- .../test/engine/triggerFailedTask.test.ts | 6 +- apps/webapp/test/engine/triggerTask.test.ts | 16 +- .../performTaskRunAlertsStoreRouting.test.ts | 2 +- .../realtime/runReaderReadThrough.test.ts | 4 +- ...ointThroughReadThrough.readthrough.test.ts | 2 +- ...ilLoaders.controlPlane.readthrough.test.ts | 13 +- apps/webapp/test/runOpsMintCutover.test.ts | 24 +-- .../test/runsReplicationInstance.test.ts | 2 +- .../test/runsRepository.readthrough.test.ts | 16 +- apps/webapp/test/sessions.readthrough.test.ts | 6 +- .../updateMetadataStoreRoutingHetero.test.ts | 2 +- .../runOpsCascadeCleanup.server.test.ts | 2 +- .../waitpointCallback.controlPlane.test.ts | 2 +- .../waitpointPresenter.readthrough.test.ts | 2 +- .../database/prisma/schema.prisma | 2 +- .../run-engine/src/engine/errors.ts | 2 +- .../src/engine/systems/debounceSystem.test.ts | 2 +- .../engine/systems/waitpointSystem.test.ts | 30 ++-- .../src/engine/systems/waitpointSystem.ts | 2 +- .../engine/tests/blockEdgeResidency.test.ts | 32 ++-- .../completeWaitpointReadResidency.test.ts | 36 ++--- .../tests/datetimeWaitpointColocation.test.ts | 36 ++--- .../src/engine/tests/lifecycleRouter.test.ts | 12 +- .../engine/tests/triggerCreateRouting.test.ts | 12 +- ...tgresRunStore.batchProbeReadClient.test.ts | 20 +-- ...stgresRunStore.controlPlaneAlertFk.test.ts | 14 +- .../PostgresRunStore.dedicatedRepro.test.ts | 86 +++++------ .../PostgresRunStore.writeAtomicity.test.ts | 20 +-- .../src/batchCompletionResidency.test.ts | 22 +-- .../runOpsStore.flipWindowDuplicate.test.ts | 16 +- ...runOpsStore.forWaitpointCompletion.test.ts | 28 ++-- .../src/runOpsStore.idempotencyDedup.test.ts | 24 +-- .../src/runOpsStore.mixedResidency.test.ts | 78 +++++----- .../src/runOpsStore.readAfterWrite.test.ts | 10 +- .../src/runOpsStore.snapshots.test.ts | 2 +- .../run-store/src/runOpsStore.test.ts | 146 +++++++++--------- .../run-store/src/runOpsStore.ts | 30 ++-- .../src/runOpsStore.waitpoints.test.ts | 6 +- internal-packages/run-store/src/types.ts | 2 +- 76 files changed, 473 insertions(+), 462 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index dc189720821..ed254be5f82 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -1727,8 +1727,8 @@ const EnvironmentSchema = z RUN_REPLICATION_LEGACY_ORIGIN_GENERATION: z.coerce.number().int().default(0), RUN_REPLICATION_NEW_ORIGIN_GENERATION: z.coerce.number().int().default(1), - // Run-ops KSUID mint cutover — per-env, canary-first, OFF by default. - // Even when on, an env mints KSUID only if its per-org runOpsMintKsuid flag is + // Run-ops id mint cutover — per-env, canary-first, OFF by default. + // Even when on, an env mints run-ops ids only if its per-org runOpsMintKsuid flag is // "ksuid" AND isSplitEnabled() is true. Cache mirrors REALTIME_BACKEND_FLAG_CACHE_*. RUN_OPS_MINT_KSUID_ENABLED: BoolEnv.default(false), RUN_OPS_MINT_FLAG_CACHE_TTL_MS: z.coerce.number().int().default(30_000), diff --git a/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts index 6d46074e726..0e04e909eb8 100644 --- a/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts @@ -131,7 +131,7 @@ export class ApiBatchResultsPresenter extends BasePresenter { } // Split: resolve the batch row new-first then off the legacy READ REPLICA only (a batch id may - // be cuid or ksuid, and a cuid-shaped id can still have been backfilled onto NEW, so id-shape + // be cuid or run-ops id, and a cuid-shaped id can still have been backfilled onto NEW, so id-shape // residency is not authoritative for the row — the new-first-then-legacy probe is), then // hydrate every member run independently via the per-run read-through primitive. async #callSplit( diff --git a/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts index c42371f632a..6619ab023a7 100644 --- a/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts @@ -42,7 +42,7 @@ export class ApiWaitpointPresenter extends BasePresenter { // Public waitpoint retrieve. Split on: new run-ops client first, then the LEGACY // RUN-OPS READ REPLICA ONLY on a new-probe miss — never the legacy primary. // Split off (single-DB / self-host): one plain waitpoint.findFirst against the replica - // (passthrough). The waitpointId is the residency-classifiable KSUID id (the route + // (passthrough). The waitpointId is the residency-classifiable run-ops id (the route // pre-decodes the friendlyId via WaitpointId.toId). const hydrate = (client: PrismaReplicaClient) => client.waitpoint.findFirst({ diff --git a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts index 8896b597fe6..fd5a8481f13 100644 --- a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts @@ -101,7 +101,7 @@ export class BatchListPresenter extends BasePresenter { } } - // codepoint comparator (NEVER localeCompare): BatchTaskRun.id is ASCII (cuid or ksuid). + // codepoint comparator (NEVER localeCompare): BatchTaskRun.id is ASCII (cuid or run-ops id). const sign = direction === "forward" ? 1 : -1; // forward => DESC; backward => ASC return Array.from(byId.values()) .sort((a, b) => (a.id < b.id ? sign : a.id > b.id ? -sign : 0)) diff --git a/apps/webapp/app/presenters/v3/BatchPresenter.server.ts b/apps/webapp/app/presenters/v3/BatchPresenter.server.ts index e0eb3a24fd1..d6af644be74 100644 --- a/apps/webapp/app/presenters/v3/BatchPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BatchPresenter.server.ts @@ -79,7 +79,7 @@ export class BatchPresenter extends BasePresenter { const batchResult = await readThrough({ // The read-through key; here it is the batch friendlyId. A cuid-shaped batch friendlyId // classifies as LEGACY and the read-through probes both stores (new first, then legacy - // replica); a ksuid-shaped one (cut-over orgs) classifies as NEW and reads only the new + // replica); a run-ops-shaped one (cut-over orgs) classifies as NEW and reads only the new // store — either way the row is found on the DB that owns it. runId: batchId, environmentId, diff --git a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts index c2efce02347..3c081b2280b 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts @@ -78,7 +78,7 @@ const { action, loader } = createActionApiRoute( } } - // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a ksuid + // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a run-ops id // run's input-stream waitpoint lands on the run's DB and its block edge resolves. const result = await engine.createManualWaitpoint({ runId: run.id, diff --git a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts index 0a34e6cddb4..f6c58ab748b 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts @@ -99,7 +99,7 @@ const { action, loader } = createActionApiRoute( } } - // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a ksuid + // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a run-ops id // run's session-stream waitpoint lands on the run's DB and its block edge resolves. const result = await engine.createManualWaitpoint({ runId: run.id, diff --git a/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts b/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts index 24f7f81ba0b..c7a8c3c5619 100644 --- a/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts +++ b/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts @@ -42,7 +42,7 @@ const { action } = createActionApiRoute( : undefined; const { waitpoint } = await engine.createDateTimeWaitpoint({ - // Co-locate the waitpoint with the run that blocks on it (run-ops split): a ksuid run lives + // Co-locate the waitpoint with the run that blocks on it (run-ops split): a run-ops run lives // on the dedicated DB, but the minted waitpoint id is always a cuid, so without the run id // the waitpoint would route to the control-plane DB and the block edge would never resolve. runId: run.id, diff --git a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts index 72bcd7d9825..8bb37adf541 100644 --- a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts +++ b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts @@ -38,7 +38,7 @@ describe("resolveIdempotencyDedupClient", () => { expect(client).toBe(FALLBACK); }); - it("routes a root run to the NEW client when the env mints ksuid", async () => { + it("routes a root run to the NEW client when the env mints run-ops ids", async () => { const client = await resolveIdempotencyDedupClient( { environmentForMint: env, parentRunFriendlyId: undefined }, makeDeps({ resolveMintKind: async () => "ksuid" }) @@ -54,10 +54,10 @@ describe("resolveIdempotencyDedupClient", () => { expect(client).toBe(LEGACY_CLIENT); }); - it("routes a child to the NEW client when the ksuid parent is NEW-resident", async () => { - const ksuidParent = RunId.toFriendlyId("a".repeat(24) + "01"); + it("routes a child to the NEW client when the run-ops parent is NEW-resident", async () => { + const runOpsParent = RunId.toFriendlyId("a".repeat(24) + "01"); const client = await resolveIdempotencyDedupClient( - { environmentForMint: env, parentRunFriendlyId: ksuidParent }, + { environmentForMint: env, parentRunFriendlyId: runOpsParent }, makeDeps({ resolveMintKind: async () => "cuid" }) // mint flag must NOT win for a child ); expect(client).toBe(NEW_CLIENT); diff --git a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts index 0011975d6d0..05498066f98 100644 --- a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts +++ b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts @@ -129,7 +129,7 @@ export class StreamBatchItemsService extends WithRunEngine { const batchId = this.parseBatchFriendlyId(batchFriendlyId); // Validate batch exists and belongs to this environment. Routed by batch id so a - // ksuid (NEW-resident) batch is found on the owning DB; the env-ownership check that + // run-ops id (NEW-resident) batch is found on the owning DB; the env-ownership check that // was in the where clause is enforced app-side below. const batch = await this._engine.runStore.findBatchTaskRunById(batchId); diff --git a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts index 0fed5c86d21..29304079aed 100644 --- a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts @@ -85,7 +85,7 @@ export class TriggerFailedTaskService { } // Mint a failed run's friendlyId. The id-kind decides which store the run is - // born in (cuid → legacy store, ksuid → new store); the whole subgraph of a + // born in (cuid → legacy store, run-ops id → new store); the whole subgraph of a // run must agree. Root failed runs mint by the environment's setting; child // failed runs inherit the parent's current store so they never split. private async mintFailedRunFriendlyId(args: { diff --git a/apps/webapp/app/runEngine/services/triggerTask.server.ts b/apps/webapp/app/runEngine/services/triggerTask.server.ts index e5586b076f4..e863013d82c 100644 --- a/apps/webapp/app/runEngine/services/triggerTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerTask.server.ts @@ -129,12 +129,12 @@ export class RunEngineTriggerTaskService { } // Mint a new run's friendlyId. The id-kind decides which store the run is born - // in (cuid → legacy store, ksuid → new store), so the whole subgraph of a run + // in (cuid → legacy store, run-ops id → new store), so the whole subgraph of a run // must agree. Two cases: // // - ROOT run (no parent): mint by the environment's cutover setting. // - CHILD run (has a parent): inherit the parent's residency by id-shape, so a - // parent and child never split across stores (ksuid parent → ksuid child, + // parent and child never split across stores (run-ops parent → run-ops child, // cuid parent → cuid child). // `region` is the caller-requested region (body.options.region). The id is // minted before the worker queue is resolved (the idempotency concern needs diff --git a/apps/webapp/app/services/realtime/sessions.server.ts b/apps/webapp/app/services/realtime/sessions.server.ts index 71170c322f2..7f50450c3a2 100644 --- a/apps/webapp/app/services/realtime/sessions.server.ts +++ b/apps/webapp/app/services/realtime/sessions.server.ts @@ -126,7 +126,7 @@ export function serializeSession(session: Session): SessionItem { * Skips the lookup when `currentRunId` is null. * * Resolves `currentRunId` -> `friendlyId` through `runStore.findRun` so a - * ksuid (NEW-DB) session run resolves from its owning store rather than the + * run-ops id (NEW-DB) session run resolves from its owning store rather than the * control-plane replica. Mirrors `sessionRunManager.server.ts`. * Tenant-scoped because `Session.currentRunId` is a no-FK pointer. */ diff --git a/apps/webapp/app/services/runsReplicationInstance.server.ts b/apps/webapp/app/services/runsReplicationInstance.server.ts index 30a757597fa..ad048422ac7 100644 --- a/apps/webapp/app/services/runsReplicationInstance.server.ts +++ b/apps/webapp/app/services/runsReplicationInstance.server.ts @@ -59,8 +59,8 @@ export function buildReplicationSources(args: { /** * The residency-split gate and the `#new`->ClickHouse replication gate are - * independent env vars. If split is on (ksuid runs are minted on the new DB) but the - * constructed sources[] has no `"new"` source, every ksuid run is silently missing from + * independent env vars. If split is on (run-ops runs are minted on the new DB) but the + * constructed sources[] has no `"new"` source, every run-ops run is silently missing from * ClickHouse — under-counting all CH-fronted usage/cost/metrics aggregates with no * Postgres fallback. Couple the gates at boot: this misconfiguration must fail loudly * rather than ship a fleet-wide under-count. @@ -69,7 +69,7 @@ export class SplitReplicationMisconfiguredError extends Error { constructor() { super( 'RUN_OPS_SPLIT_ENABLED is on but the runs-replication sources[] has no "new" source: ' + - "ksuid runs on the new DB would not replicate to ClickHouse, under-counting every " + + "run-ops runs on the new DB would not replicate to ClickHouse, under-counting every " + "ClickHouse-fronted aggregate. Enable the new replication source " + "(RUN_REPLICATION_NEW_ENABLED / RUN_OPS_DATABASE_URL) or turn the split off." ); diff --git a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts index 65df2b41215..f57e37275c3 100644 --- a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts @@ -194,7 +194,7 @@ export class ClickHouseRunsRepository implements IRunsRepository { // Preserve the ClickHouse keyset order (created_at desc, run_id desc) by re-ordering the // hydrated rows to match the input `runIds`. Sorting by raw `id` was only ~chronological - // when every id was a time-prefixed cuid; a mixed cuid/ksuid page sorts the two id-spaces + // when every id was a time-prefixed cuid; a mixed cuid/run-ops id page sorts the two id-spaces // into separate blocks, burying recent runs. Rows whose PG row is gone (e.g. past // retention) drop out, exactly as before. const byId = new Map(rows.map((r) => [r.id, r] as const)); diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts index 2fadb8c7108..fd095150ac0 100644 --- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts @@ -295,7 +295,7 @@ export async function convertRunListInputOptionsToFilterRunsOptions( }); convertedOptions.period = time.period ? (parseDuration(time.period) ?? undefined) : undefined; - // Cross-DB resolution: BatchTaskRun is a RUN-OPS table. A ksuid batch resident on the + // Cross-DB resolution: BatchTaskRun is a RUN-OPS table. A run-ops batch resident on the // dedicated run-ops DB must resolve via the store's NEW->LEGACY probe — a single control-plane // client would miss it and leave the friendlyId in the ClickHouse `batch_id` filter, matching // nothing. Split off / self-host: the store is a passthrough over the one client. diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index 4617179eda1..f85ba79e456 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -51,7 +51,7 @@ export const FeatureFlagCatalog = { [FEATURE_FLAG.computeMigrationRequireTemplate]: z.boolean(), // Per-org access to development branches. Off unless enabled for the org. [FEATURE_FLAG.devBranchesEnabled]: z.coerce.boolean(), - // Per-org KSUID mint cutover. Defaults to "cuid"; only honored when + // Per-org run-ops-id mint cutover. Defaults to "cuid"; only honored when // RUN_OPS_MINT_KSUID_ENABLED is on AND isSplitEnabled() is true. [FEATURE_FLAG.runOpsMintKsuid]: z.enum(["cuid", "ksuid"]), }; diff --git a/apps/webapp/app/v3/runEngineHandlersShared.server.ts b/apps/webapp/app/v3/runEngineHandlersShared.server.ts index 338bbd2f9fc..3493a52ad2f 100644 --- a/apps/webapp/app/v3/runEngineHandlersShared.server.ts +++ b/apps/webapp/app/v3/runEngineHandlersShared.server.ts @@ -71,7 +71,7 @@ export async function readRunForEventOrThrow( * Resolve which run-ops writer physically owns the `BatchTaskRun` row for * `batchId` by probing where the row lives, so the batch-completion txn commits * on a single run-ops DB. Length classification is INVALID here: a batch id may - * be a ksuid (cut-over orgs) or a cuid (and cuid-shaped ids can be backfilled + * be a run-ops id (cut-over orgs) or a cuid (and cuid-shaped ids can be backfilled * onto NEW), so id-shape does not reliably indicate the row's actual residency. * The existence probe is the correct signal. */ diff --git a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts index e3e2e2c1f99..168647af4f4 100644 --- a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts @@ -3,7 +3,7 @@ import { batchIdForMintKind, resolveBatchMintKind } from "./mintBatchFriendlyId. import { classifyKind } from "@trigger.dev/core/v3/isomorphic"; describe("batchIdForMintKind (pure)", () => { - it("ksuid -> 26-char classifiable NEW batch id (no 21-char ids)", () => { + it("'ksuid' kind -> 26-char classifiable NEW batch id (no 21-char ids)", () => { const r = batchIdForMintKind("ksuid"); expect(r.friendlyId.startsWith("batch_")).toBe(true); expect(r.id.length).toBe(26); @@ -51,7 +51,7 @@ describe("resolveBatchMintKind", () => { expect(kind).toBe("cuid"); }); - it("CHILD batch inherits a ksuid (NEW) parent by id-shape", async () => { + it("CHILD batch inherits a run-ops (NEW) parent by id-shape", async () => { const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`; const resolveRunIdMintKind = vi.fn(); @@ -81,7 +81,7 @@ describe("resolveBatchMintKind", () => { // mint-on-FLIP invariant: a child follows its parent's store even after the org flag // flips the other way. The flag resolver must NEVER be consulted for a child. - it("FLIP cuid->ksuid: a cuid (LEGACY) parent still mints a cuid child though the flag now says ksuid", async () => { + it("FLIP 'cuid'->'ksuid': a cuid (LEGACY) parent still mints a cuid child though the flag now says 'ksuid'", async () => { const parentRunFriendlyId = `run_${"a".repeat(25)}`; const resolveRunIdMintKind = vi.fn().mockResolvedValue("ksuid"); // flag flipped to ksuid const kind = await resolveBatchMintKind({ @@ -93,7 +93,7 @@ describe("resolveBatchMintKind", () => { expect(resolveRunIdMintKind).not.toHaveBeenCalled(); }); - it("FLIP ksuid->cuid: a ksuid (NEW) parent still mints a ksuid child though the flag now says cuid", async () => { + it("FLIP 'ksuid'->'cuid': a run-ops (NEW) parent still mints a run-ops child though the flag now says 'cuid'", async () => { const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`; const resolveRunIdMintKind = vi.fn().mockResolvedValue("cuid"); // flag flipped back to cuid const kind = await resolveBatchMintKind({ diff --git a/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts b/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts index 8b83b70d78f..d173f3958bd 100644 --- a/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts @@ -4,7 +4,7 @@ * is false (single-DB passthrough). * * During the retention window, old run-ops rows are served off the legacy read replica. - * Residency is decided purely by id-shape: a ksuid (NEW) id reads new only, a cuid + * Residency is decided purely by id-shape: a run-ops id (NEW) id reads new only, a cuid * (LEGACY) id reads legacy only. An unclassifiable id falls back to a new-then-legacy * probe. After termination, past-retention runs return the normal not-found response. * Patterned on `mollifier/resolveRunForMutation.server.ts` (`?? default` DI), but with @@ -80,7 +80,7 @@ export async function readThroughRun( } } - // A ksuid id can only live on the new DB — skip the legacy replica entirely. + // A run-ops id can only live on the new DB — skip the legacy replica entirely. if (residency === "NEW") { const v = await input.readNew(newClient); return v != null ? { source: "new", value: v } : { source: "not-found" }; diff --git a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts index 9fbd5c640a8..b7f6e313e49 100644 --- a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; import { resolveInheritedMintKind } from "./resolveInheritedMintKind.server"; -const NEW_PARENT = `run_${"a".repeat(24) + "01"}`; // ksuid id-shape -> NEW +const NEW_PARENT = `run_${"a".repeat(24) + "01"}`; // run-ops id-shape -> NEW const LEGACY_PARENT = `run_${"b".repeat(25)}`; // cuid id-shape -> LEGACY describe("resolveInheritedMintKind (pure id-shape, shared across all mint paths)", () => { - it("inherits a ksuid (NEW) parent by id-shape -> ksuid", () => { + it("inherits a run-ops (NEW) parent by id-shape -> 'ksuid' kind", () => { expect(resolveInheritedMintKind(NEW_PARENT)).toBe("ksuid"); }); diff --git a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts index e43c3a8e33c..63d689eae39 100644 --- a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts @@ -4,7 +4,7 @@ import type { RunIdMintKind } from "./runOpsMintKind.server"; // Mint a child in the SAME physical store as its anchor (parent run / owning batch), // regardless of the org's current mint flag — keeps a subgraph co-resident across a // flip. With no migration/drain, residency is a pure id-shape check (zero hot-path -// I/O): a ksuid (NEW) parent mints ksuid children, a cuid (LEGACY) parent mints cuid. +// I/O): a run-ops (NEW) parent mints run-ops children, a cuid (LEGACY) parent mints cuid. export function resolveInheritedMintKind(parentRunFriendlyId: string): RunIdMintKind { return ownerEngine(parentRunFriendlyId) === "NEW" ? "ksuid" : "cuid"; } diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts b/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts index 2392d516180..6d08350f62d 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts @@ -73,7 +73,7 @@ void _legacyWriterAssignable; * * Deletes route through the dedicated run-ops write clients (`runOpsNewPrismaClient` + * `runOpsLegacyPrisma`), NOT the control-plane `prisma`. The ordered delete pass runs against BOTH - * writers: a migrating env/project's run-ops rows split across the new (KSUID) and + * writers: a migrating env/project's run-ops rows split across the new (run-ops id) and * legacy (cuid) DBs per the per-env cutover + roll-new-forward rollback, and the * cloud DB that lost its physical FK has no cascade to clean the other writer's miss. In single-DB * both handles are reference-equal to the one collapsed client, so de-dup-by-reference runs the diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts index fc346dad897..07ead53aad0 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts @@ -31,7 +31,7 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window — current be beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); - it("returns the STALE cached kind within the TTL after the flag flips cuid->ksuid", async () => { + it("returns the STALE cached kind within the TTL after the flag flips 'cuid'->'ksuid'", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); let live: RunIdMintKind = "cuid"; const flag = makeCachedFlag(cache, () => live); @@ -44,7 +44,7 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window — current be expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // STALE, as designed }); - it("returns the FRESH kind once the TTL expires after a cuid->ksuid flip", async () => { + it("returns the FRESH kind once the TTL expires after a 'cuid'->'ksuid' flip", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); let live: RunIdMintKind = "cuid"; const flag = makeCachedFlag(cache, () => live); @@ -57,7 +57,7 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window — current be expect(await computeRunIdMintKind(env, deps)).toBe("ksuid"); // re-reads the live flag }); - it("symmetric flip-back ksuid->cuid is also stale within TTL, fresh after", async () => { + it("symmetric flip-back 'ksuid'->'cuid' is also stale within TTL, fresh after", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); let live: RunIdMintKind = "ksuid"; const flag = makeCachedFlag(cache, () => live); diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts index 9d2e575fef8..c539ee9d4a9 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts @@ -12,7 +12,7 @@ describe("computeRunIdMintKind (pure)", () => { expect(flag).not.toHaveBeenCalled(); }); - it("mints cuid when split is OFF, even if master + per-org flag say ksuid", async () => { + it("mints cuid when split is OFF, even if master + per-org flag say 'ksuid'", async () => { const flag = vi.fn().mockResolvedValue("ksuid"); const kind = await computeRunIdMintKind( { organizationId: "org_1", id: "env_1" }, @@ -22,7 +22,7 @@ describe("computeRunIdMintKind (pure)", () => { expect(flag).not.toHaveBeenCalled(); // split-off short-circuits before any flag read }); - it("mints ksuid only when master on AND split on AND per-org flag = ksuid", async () => { + it("mints run-ops id only when master on AND split on AND per-org flag = 'ksuid'", async () => { const flag = vi.fn().mockResolvedValue("ksuid"); const kind = await computeRunIdMintKind( { organizationId: "org_1", id: "env_1" }, diff --git a/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts b/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts index c1bf592e4a4..955bd90b94a 100644 --- a/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts @@ -47,7 +47,7 @@ export type SplitRealtimeInterlockConfig = { /** * Boot-time realtime interlock (pure predicate). Split mode puts NEW-resident - * (ksuid) runs on the dedicated run-ops DB, but Electric replicates only from the + * (run-ops id) runs on the dedicated run-ops DB, but Electric replicates only from the * control-plane DB — with the native realtime backend OFF those runs are invisible * and every realtime subscription hangs. Refuse split unless native is on; split-off * is always allowed regardless of the realtime backend. diff --git a/apps/webapp/app/v3/runStore.server.test.ts b/apps/webapp/app/v3/runStore.server.test.ts index ea8d9171974..3f01ceeed0b 100644 --- a/apps/webapp/app/v3/runStore.server.test.ts +++ b/apps/webapp/app/v3/runStore.server.test.ts @@ -80,11 +80,11 @@ function createRunInput(params: { }; } -describe("T24 — findRun resolves ksuid run on dedicated DB", () => { +describe("T24 — findRun resolves run-ops run on dedicated DB", () => { heteroRunOpsPostgresTest( - "split ON: findRun({friendlyId, runtimeEnvironmentId}, {select}) finds a ksuid run on the new store", + "split ON: findRun({friendlyId, runtimeEnvironmentId}, {select}) finds a run-ops run on the new store", async ({ prisma14, prisma17 }) => { - const ENV_ID = "env_t24_ksuid_probe"; + const ENV_ID = "env_t24_runops_probe"; const WORKER_ID = "worker_t24_lock"; await prisma17.taskRun.create({ data: { @@ -147,7 +147,7 @@ describe("buildRunStore", () => { expect(store).toBeInstanceOf(PostgresRunStore); const seed = await seedEnvironment(prisma14, "off"); - // A ksuid id (would route to NEW under split) must still land on the single DB. + // A run-ops id (would route to NEW under split) must still land on the single DB. const runId = NEW_ID_26; await store.createRun( createRunInput({ @@ -183,7 +183,7 @@ describe("buildRunStore", () => { const seedNew = await seedEnvironment(prisma17, "on_new"); const seedLegacy = await seedEnvironment(prisma14, "on_legacy"); - // ksuid -> NEW (PG17) + // run-ops id -> NEW (PG17) await store.createRun( createRunInput({ runId: NEW_ID_26, diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index 8e4ec92fae9..53e03d17b08 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -344,8 +344,8 @@ export class BatchTriggerV3Service extends BaseService { } // Mint a child run's friendlyId so it lands in the SAME physical store as its - // residency anchor. The caller passes the batch's friendlyId, so a ksuid - // (NEW) anchor yields a ksuid (NEW) child and a cuid anchor yields a cuid + // residency anchor. The caller passes the batch's friendlyId, so a run-ops id + // (NEW) anchor yields a run-ops id (NEW) child and a cuid anchor yields a cuid // (LEGACY) child. With no anchor it falls back to the env's cutover setting. // Mirrors RunEngineTriggerTaskService.mintRunFriendlyId. private async mintChildFriendlyId( @@ -1002,7 +1002,7 @@ export async function tryCompleteBatchV3( batchId: string, tx: PrismaClientOrTransaction, scheduleResumeOnComplete: boolean, - // Threaded in so a ksuid (NEW-resident) batch + its items are read/written on the owning + // Threaded in so a run-ops id (NEW-resident) batch + its items are read/written on the owning // store, not the control-plane `tx`. Defaults to the singleton (single-DB = passthrough). runStore: RunStore = defaultRunStore ) { @@ -1062,7 +1062,7 @@ export async function completeBatchTaskRunItemV3( scheduleResumeOnComplete = false, taskRunAttemptId?: string, retryAttempt?: number, - // Threaded in so a ksuid (NEW-resident) batch's item lands on the owning store; route by + // Threaded in so a run-ops id (NEW-resident) batch's item lands on the owning store; route by // batchTaskRunId (items co-reside with their batch). Defaults to the singleton. runStore: RunStore = defaultRunStore ) { diff --git a/apps/webapp/app/v3/services/createCheckpoint.server.ts b/apps/webapp/app/v3/services/createCheckpoint.server.ts index 43ff25f2053..088001b8267 100644 --- a/apps/webapp/app/v3/services/createCheckpoint.server.ts +++ b/apps/webapp/app/v3/services/createCheckpoint.server.ts @@ -146,7 +146,7 @@ export class CreateCheckpointService extends BaseService { break; } case "WAIT_FOR_BATCH": { - // Routed by friendlyId so a ksuid (NEW-resident) batch is found on the owning DB; + // Routed by friendlyId so a run-ops id (NEW-resident) batch is found on the owning DB; // env-scoped to the dependent attempt's run (a batch shares its dependent's env). const batchRun = await this.runStore.findBatchTaskRunByFriendlyId( reason.batchFriendlyId, @@ -361,7 +361,7 @@ export class CreateCheckpointService extends BaseService { }); await marqs?.cancelHeartbeat(attempt.taskRunId); - // Routed by friendlyId so a ksuid (NEW-resident) batch is found on the owning DB; + // Routed by friendlyId so a run-ops id (NEW-resident) batch is found on the owning DB; // env-scoped to the dependent attempt's run (a batch shares its dependent's env). const batchRun = await this.runStore.findBatchTaskRunByFriendlyId( reason.batchFriendlyId, diff --git a/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts b/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts index daed805b1ae..4a123ff777d 100644 --- a/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts +++ b/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts @@ -72,7 +72,7 @@ export class ExecuteTasksWaitingForDeployService extends BaseService { // Defense-in-depth: the open-predicate findRuns fan-out can select runs from // either DB, but the status flip below is a single control-plane updateMany. A - // ksuid (NEW-resident) run can only reach WAITING_FOR_DEPLOY via a misconfiguration + // run-ops id (NEW-resident) run can only reach WAITING_FOR_DEPLOY via a misconfiguration // (it is a V1/cuid-only status — V2 uses PENDING_VERSION). Surface it loudly rather // than silently strand the run, and only mutate the LEGACY-resident runs the // control-plane client can actually reach. diff --git a/apps/webapp/test/SpanPresenter.readthrough.test.ts b/apps/webapp/test/SpanPresenter.readthrough.test.ts index 10a402189b2..2e6054f0423 100644 --- a/apps/webapp/test/SpanPresenter.readthrough.test.ts +++ b/apps/webapp/test/SpanPresenter.readthrough.test.ts @@ -222,7 +222,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { const ctxNew = await seedParents(prisma17, "partn"); await mirrorParents(prisma14, ctxNew, "partn"); // legacy run-ops + CP parents share ids - const runId = `run_${NEW_ID_26}`; // ksuid → NEW residency + const runId = `run_${NEW_ID_26}`; // run-ops id → NEW residency const childMigratedId = `run_a${NEW_ID_26.slice(1)}`; // also NEW const parentFriendlyId = `run_p${NEW_ID_26.slice(1)}`; // v1 body → routes NEW by friendlyId await createRun(prisma17, ctxNew, { @@ -385,7 +385,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { async ({ prisma14, prisma17 }) => { const ctx = await seedParents(prisma17, "knownmig"); - const newRunId = `run_${NEW_ID_26}`; // ksuid → NEW residency + const newRunId = `run_${NEW_ID_26}`; // run-ops id → NEW residency await createRun(prisma17, ctx, { id: newRunId, friendlyId: "run_known_new", @@ -494,7 +494,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { await mirrorParents(prisma17, ctx, "e2e4"); const parentId = `run_${CUID_25}`; // cuid → LEGACY (in-retention) - const childId = `run_${NEW_ID_26}`; // ksuid → NEW (born-new) + const childId = `run_${NEW_ID_26}`; // run-ops id → NEW (born-new) await createRun(prisma14, ctx, { id: parentId, @@ -536,7 +536,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { // tree's FK self-relations stay single-DB. expect(parent?.rootTaskRun?.friendlyId).toBe("run_e2e_parent"); - // The child resolves from the NEW slot (routed by its ksuid id) and points back at the parent + // The child resolves from the NEW slot (routed by its run-ops id) and points back at the parent // span — the cross-the-line parent/child shape, with no cross-DB join. const child = await store.findRun( { id: childId }, diff --git a/apps/webapp/test/api.v1.waitpoints.tokens.test.ts b/apps/webapp/test/api.v1.waitpoints.tokens.test.ts index 1bd8948d6b5..e5df494885e 100644 --- a/apps/webapp/test/api.v1.waitpoints.tokens.test.ts +++ b/apps/webapp/test/api.v1.waitpoints.tokens.test.ts @@ -2,7 +2,7 @@ import { describe, expect, vi } from "vitest"; // Store-routed engine create/get seam + the residency-keyed id contract behind // the create route (not its HTTP action). A standalone MANUAL token is cuid → -// LEGACY; NEW residency is reached only by co-locating the token with a ksuid run. +// LEGACY; NEW residency is reached only by co-locating the token with a run-ops run. import { RunEngine } from "@internal/run-engine"; import { setupAuthenticatedEnvironment } from "@internal/run-engine/tests"; @@ -280,7 +280,7 @@ function buildCreateRunInput(params: { }; } -async function seedExecutingKsuidRun( +async function seedExecutingRunOpsRun( prisma14: PrismaClient, router: RoutingRunStore, runId: string, @@ -329,21 +329,21 @@ function makeRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { return new RoutingRunStore({ new: newStore, legacy: legacyStore }); } -describe("waitpoint-token create engine seam — NEW residency via a ksuid run across the version boundary", () => { - // NEW residency comes from co-locating the token with a ksuid run; the token +describe("waitpoint-token create engine seam — NEW residency via a run-ops run across the version boundary", () => { + // NEW residency comes from co-locating the token with a run-ops run; the token // resolves only on its owning (#new) store across the PG14<->PG17 boundary, never #legacy. twoDbEngineTest( - "a ksuid run's token co-locates on #new and resolves only there, not on #legacy", + "a run-ops run's token co-locates on #new and resolves only there, not on #legacy", async ({ prisma14, prisma17, redisOptions }) => { const p14 = prisma14 as unknown as PrismaClient; const router = makeRouter(p14, prisma17); const engine = buildEngine({ prisma: prisma14, redisOptions, store: router }); try { - // A NEW-classified run id (explicit ksuid), mirroring the trigger-routing helper. + // A NEW-classified run id (explicit run-ops id), mirroring the trigger-routing helper. const runId = RunId.toFriendlyId(generateRunOpsId()); expect(ownerEngine(runId)).toBe("NEW"); - const env = await seedExecutingKsuidRun(p14, router, runId, "wpnew"); + const env = await seedExecutingRunOpsRun(p14, router, runId, "wpnew"); const { waitpoint } = await engine.createManualWaitpoint({ runId, diff --git a/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts b/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts index dfec32668e8..cd1ac3210bd 100644 --- a/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts +++ b/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts @@ -1,6 +1,6 @@ // Route-level regression for ApiBatchResultsPresenter: the /batches/:id/results route used to build // the presenter with no read-through deps, collapsing to a passthrough read off the control-plane -// replica only, which 404s a NEW-resident (ksuid) batch that lives on the dedicated run-ops DB. +// replica only, which 404s a NEW-resident (run-ops id) batch that lives on the dedicated run-ops DB. import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; import { describe, expect, vi } from "vitest"; diff --git a/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts b/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts index fbf685f2b16..530ef8234de 100644 --- a/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts @@ -6,6 +6,7 @@ // boundaries (splitEnabled/isPastRetention) are injected. import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { customAlphabet } from "nanoid"; import { describe, expect, vi } from "vitest"; import { ApiRunResultPresenter } from "~/presenters/v3/ApiRunResultPresenter.server"; @@ -21,10 +22,11 @@ vi.setConfig({ testTimeout: 60_000 }); const idGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", 21); -// Residency by friendlyId length (after stripping `run_`): 26-char v1 body (version "1" at index 25) → NEW, -// 25-char body → LEGACY (cuid analog). ownerEngine classifies on the public friendly id. +// Residency by friendlyId shape (after stripping `run_`): a valid 26-char v1 body (version "1" at +// index 25, base32hex core) → NEW; a 25-char body → LEGACY (cuid analog). ownerEngine classifies on +// the public friendly id, so newFriendlyId uses the real generator to produce a NEW-classified body. function newFriendlyId(): string { - return "run_" + customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 27)(); + return "run_" + generateRunOpsId(); } function legacyFriendlyId(): string { return "run_" + customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 25)(); diff --git a/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts b/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts index 00b1d6ea650..44d6a783b9c 100644 --- a/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts @@ -111,7 +111,7 @@ const environmentArg = (env: { id: string; projectId: string }) => ({ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgres)", () => { heteroPostgresTest( - "resolves on run-ops NEW (ksuid), legacy replica never touched", + "resolves on run-ops NEW (run-ops id), legacy replica never touched", async ({ prisma17, prisma14 }) => { const id = generateRunOpsId(); expect(id.length).toBe(26); @@ -139,7 +139,7 @@ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgre expect(result.tags).toEqual(["x", "y", "z"]); expect(result.output).toBe(JSON.stringify({ n: 42 })); expect(result.type).toBe("MANUAL"); - // ksuid → NEW: new store served the read, legacy never touched (fast-path). + // run-ops id → NEW: new store served the read, legacy never touched (fast-path). expect(newClient.calls.length).toBe(1); expect(legacy.calls.length).toBe(0); } diff --git a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts index 875ac58613d..c8492e7fa26 100644 --- a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts +++ b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts @@ -20,11 +20,11 @@ import { BatchTriggerV3Service } from "~/v3/services/batchTriggerV3.server"; vi.setConfig({ testTimeout: 60_000 }); const CUID_LEN = 25; -const KSUID_LEN = 26; +const RUN_OPS_ID_LEN = 26; // Minimal AuthenticatedEnvironment — only the fields the mint path reads // (organizationId, id, organization.featureFlags) need to be real. A root batch -// (no parentRunId) with no ksuid override mints cuid, which is the env-default +// (no parentRunId) with no run-ops id override mints cuid, which is the env-default // branch we assert on below. function fakeEnv(): AuthenticatedEnvironment { return { @@ -42,17 +42,17 @@ function buildService() { } describe("BatchTriggerV3Service child-residency inheritance", () => { - it("a ksuid parent yields ksuid (NEW) child friendlyIds", async () => { + it("a run-ops parent yields run-ops id (NEW) child friendlyIds", async () => { const service = buildService(); const parentFriendlyId = RunId.toFriendlyId( // v1 internal id (version "1" at index 25) → NEW residency parent - "a".repeat(KSUID_LEN - 1) + "1" + "a".repeat(RUN_OPS_ID_LEN - 1) + "1" ); expect(ownerEngine(RunId.fromFriendlyId(parentFriendlyId))).toBe("NEW"); const childFriendlyId = await (service as any).mintChildFriendlyId(fakeEnv(), parentFriendlyId); - expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(KSUID_LEN); + expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(RUN_OPS_ID_LEN); expect(ownerEngine(RunId.fromFriendlyId(childFriendlyId))).toBe("NEW"); }); @@ -76,21 +76,21 @@ describe("BatchTriggerV3Service child-residency inheritance", () => { // A root batch's children are anchored to the batch's friendlyId, NOT to a // re-resolution of the per-org flag. Even with the env flag forced to "cuid" (a flip - // away from the batch's residency), a ksuid batch anchor yields ksuid children — so + // away from the batch's residency), a run-ops batch anchor yields run-ops children — so // batch + children stay co-resident and TaskRun.batchId never crosses the seam. - it("a ksuid batch anchor yields ksuid children even when the env flag resolves cuid", async () => { + it("a run-ops batch anchor yields run-ops children even when the env flag resolves cuid", async () => { const service = buildService(); // resolveMintKind forced to "cuid" - const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId()); // ksuid (NEW) batch + const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId()); // run-ops id (NEW) batch expect(ownerEngine(batchFriendlyId)).toBe("NEW"); const childFriendlyId = await (service as any).mintChildFriendlyId(fakeEnv(), batchFriendlyId); - expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(KSUID_LEN); + expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(RUN_OPS_ID_LEN); expect(ownerEngine(RunId.fromFriendlyId(childFriendlyId))).toBe("NEW"); }); // The cuid mirror: a cuid batch anchor yields cuid children even if the flag flipped ON. - it("a cuid batch anchor yields cuid children even when the env flag resolves ksuid", async () => { + it("a cuid batch anchor yields cuid children even when the env flag resolves 'ksuid'", async () => { const service = new BatchTriggerV3Service( undefined, undefined, diff --git a/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts b/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts index 55fc919558b..9c91385071f 100644 --- a/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts +++ b/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts @@ -90,7 +90,7 @@ function recording(client: PrismaClient, opts: { forbidden?: boolean } = {}) { describe("CancelDevSessionRunsService store routing (hetero)", () => { heteroPostgresTest( - "a NEW run (ksuid) resolves on the new store via read-through, by friendlyId and by id", + "a NEW run (run-ops id) resolves on the new store via read-through, by friendlyId and by id", async ({ prisma17, prisma14 }) => { const id = generateRunOpsId(); expect(id.length).toBe(26); @@ -127,7 +127,7 @@ describe("CancelDevSessionRunsService store routing (hetero)", () => { cancelledAt: new Date(), reason: "test", }); - // ksuid → NEW: new store served the read, legacy never touched. + // run-ops id → NEW: new store served the read, legacy never touched. expect(newClient.calls.length).toBe(1); expect(legacy.calls.length).toBe(0); } diff --git a/apps/webapp/test/engine/triggerFailedTask.test.ts b/apps/webapp/test/engine/triggerFailedTask.test.ts index 17568959293..2576e69863c 100644 --- a/apps/webapp/test/engine/triggerFailedTask.test.ts +++ b/apps/webapp/test/engine/triggerFailedTask.test.ts @@ -86,7 +86,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { ); containerTest( - "failed child of a NEW (ksuid) parent mints ksuid (call)", + "failed child of a NEW (run-ops id) parent mints run-ops id (call)", async ({ prisma, redisOptions }) => { const engine = makeEngine(prisma, redisOptions); const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); @@ -182,7 +182,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { ); containerTest( - "failed child of a NEW parent mints ksuid (callWithoutTraceEvents)", + "failed child of a NEW parent mints run-ops id (callWithoutTraceEvents)", async ({ prisma, redisOptions }) => { const engine = makeEngine(prisma, redisOptions); const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); @@ -236,7 +236,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { const service = makeService(prisma, engine); - // A well-formed ksuid parent friendlyId that was NEVER triggered → no row. + // A well-formed run-ops parent friendlyId that was NEVER triggered → no row. // Exercises the missing-parent fallback in callWithoutTraceEvents. const absentParentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); diff --git a/apps/webapp/test/engine/triggerTask.test.ts b/apps/webapp/test/engine/triggerTask.test.ts index e95a7e746e5..2d235bcc1eb 100644 --- a/apps/webapp/test/engine/triggerTask.test.ts +++ b/apps/webapp/test/engine/triggerTask.test.ts @@ -2385,20 +2385,18 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = ); containerTest( - "child of a NEW (ksuid) parent is minted ksuid (born NEW)", + "child of a NEW (run-ops id) parent is minted run-ops id (born NEW)", async ({ prisma, redisOptions }) => { const { engine, authenticatedEnvironment, taskIdentifier, triggerTaskService } = await setupResidencyService(prisma, redisOptions); - // Construct a NEW-resident parent directly by minting a ksuid friendlyId + // Construct a NEW-resident parent directly by minting a run-ops id friendlyId // and creating its run row, so the child inherits NEW by id-shape alone - // (no marker needed). We trigger the parent with an explicit ksuid id via + // (no marker needed). We trigger the parent with an explicit run-ops id via // the runFriendlyId option so the row physically exists for the parent // lookup the child path performs. - const parentFriendlyId = RunId.toFriendlyId( - // v1 id (version "1" at index 25) → classifies NEW - (await import("@trigger.dev/core/v3/isomorphic")).generateRunOpsId() - ); + // v1 id (version "1" at index 25) → classifies NEW + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); expect(classifyKind(parentFriendlyId)).toBe("ksuid"); const parent = await triggerTaskService.call({ @@ -2427,7 +2425,7 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = const { engine, authenticatedEnvironment, taskIdentifier, triggerTaskService } = await setupResidencyService(prisma, redisOptions); - // Explicit cuid id for the run, and a ksuid/NEW parent id. + // Explicit cuid id for the run, and a run-ops id/NEW parent id. const explicitFriendlyId = RunId.toFriendlyId(generateInternalId()); const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); expect(classifyKind(explicitFriendlyId)).toBe("cuid"); @@ -2440,7 +2438,7 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = options: { runFriendlyId: explicitFriendlyId }, }); - // Caller-supplied id wins verbatim — NOT re-minted to ksuid despite the NEW parent. + // Caller-supplied id wins verbatim — NOT re-minted to run-ops id despite the NEW parent. expect(result!.run.friendlyId).toBe(explicitFriendlyId); await engine.quit(); diff --git a/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts b/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts index ed787232c7a..c6db85d5215 100644 --- a/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts +++ b/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts @@ -1,6 +1,6 @@ // Real heterogeneous legacy + new Postgres proof for the alert-hydration TaskRun read. // The DB is never mocked. A test-only RunStore wraps two real PostgresRunStore -// instances and routes findRun by id residency (ksuid → NEW, cuid → LEGACY), +// instances and routes findRun by id residency (run-ops id → NEW, cuid → LEGACY), // mirroring the sibling routing suite. The ProjectAlertChannel read must stay control-plane. // // The alert env-type read (parentEnvironment?.type ?? type) is resolved via the app diff --git a/apps/webapp/test/realtime/runReaderReadThrough.test.ts b/apps/webapp/test/realtime/runReaderReadThrough.test.ts index 978f3eedc44..cd23d954fed 100644 --- a/apps/webapp/test/realtime/runReaderReadThrough.test.ts +++ b/apps/webapp/test/realtime/runReaderReadThrough.test.ts @@ -16,7 +16,7 @@ import { RunHydrator } from "~/services/realtime/runReader.server"; // (which live in the hydrator, not the store) are unaffected by the seam. // // The heterogeneous fixture gives real legacy + new Postgres containers; NO DB is mocked. The ONLY -// non-DB fake is the residency selector that the routing-shaped store uses (`ownerEngine`: ksuid -> +// non-DB fake is the residency selector that the routing-shaped store uses (`ownerEngine`: run-ops id -> // NEW, cuid -> LEGACY), exactly the substrate the RoutingRunStore ships. Run ids are 25 chars (cuid // -> LEGACY) or v1-shaped (26 chars, version "1" at index 25 -> NEW) so the classifier routes them deterministically. @@ -310,7 +310,7 @@ describe("RunHydrator read-route through the runStore seam (legacy + new)", () = } ); - // Terminal-metadata read-seam: a NEW-resident (ksuid) run's final metadata is hydrated through + // Terminal-metadata read-seam: a NEW-resident (run-ops id) run's final metadata is hydrated through // the owning (NEW) store, not off a generic legacy replica. Asserts read-seam ROUTING for the // terminal read; it is not a hard ordering/consistency guarantee about when the terminal marker // and the row's terminal columns converge. diff --git a/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts b/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts index fed06cb0ac6..4bceb951525 100644 --- a/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts +++ b/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts @@ -84,7 +84,7 @@ const read = (waitpointId: string, environmentId: string) => (client: PrismaRepl describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run-ops PG17)", () => { heteroRunOpsPostgresTest( - "ksuid waitpoint resolves on the dedicated run-ops client; legacy replica never touched", + "run-ops waitpoint resolves on the dedicated run-ops client; legacy replica never touched", async ({ prisma17, prisma14 }) => { const id = generateRunOpsId(); expect(id.length).toBe(26); diff --git a/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts b/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts index d2bf3d6e193..76898104f03 100644 --- a/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts +++ b/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts @@ -50,7 +50,10 @@ async function seedAll(prisma: PrismaClient) { // The run lives on the dedicated run-ops client; its control-plane FKs are synthetic scalar ids // pointing at rows that exist only on PG14 (the dedicated DB has no such tables). -async function seedKsuidRun(prisma17: RunOpsPrismaClient, cp: Awaited>) { +async function seedRunOpsRun( + prisma17: RunOpsPrismaClient, + cp: Awaited> +) { const k = n++; return prisma17.taskRun.create({ data: { @@ -91,11 +94,11 @@ function wire(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { describe("run-detail loaders cross-DB read-through (dedicated run-ops client)", () => { heteroRunOpsPostgresTest( - "ksuid run resolves: friendlyId read on the dedicated run-ops DB + membership/env auth on PG14 (resources.runs.$runParam shape)", + "run-ops run resolves: friendlyId read on the dedicated run-ops DB + membership/env auth on PG14 (resources.runs.$runParam shape)", async ({ prisma14, prisma17 }) => { const cp14 = prisma14 as unknown as PrismaClient; const cp = await seedAll(cp14); - const run = await seedKsuidRun(prisma17, cp); + const run = await seedRunOpsRun(prisma17, cp); const { runStore, resolver } = wire(cp14, prisma17); const found = await runStore.findRun( @@ -141,7 +144,7 @@ describe("run-detail loaders cross-DB read-through (dedicated run-ops client)", async ({ prisma14, prisma17 }) => { const cp14 = prisma14 as unknown as PrismaClient; const cp = await seedAll(cp14); - const run = await seedKsuidRun(prisma17, cp); + const run = await seedRunOpsRun(prisma17, cp); const { runStore } = wire(cp14, prisma17); const found = await runStore.findRun( @@ -166,7 +169,7 @@ describe("run-detail loaders cross-DB read-through (dedicated run-ops client)", async ({ prisma14, prisma17 }) => { const cp14 = prisma14 as unknown as PrismaClient; const cp = await seedAll(cp14); - const run = await seedKsuidRun(prisma17, cp); + const run = await seedRunOpsRun(prisma17, cp); const { runStore, resolver } = wire(cp14, prisma17); const found = await runStore.findRun( diff --git a/apps/webapp/test/runOpsMintCutover.test.ts b/apps/webapp/test/runOpsMintCutover.test.ts index ca7cbb6b9c9..86c824862e9 100644 --- a/apps/webapp/test/runOpsMintCutover.test.ts +++ b/apps/webapp/test/runOpsMintCutover.test.ts @@ -1,10 +1,10 @@ -// Per-env KSUID mint cutover integration proof. +// Per-env run-ops-id mint cutover integration proof. // // NEVER mocks the DB: the mint decision runs through the pure core `computeRunIdMintKind` // wired to a REAL `makeFlag(prisma)` that reads the REAL `Organization.featureFlags` / // `FeatureFlag` rows in a testcontainers Postgres. Only the two boundary knobs // are injected — `masterEnabled` and the `splitEnabled` boot-boolean — never a -// mocked DB. The KSUID/cuid format + residency are then proven through the SAME isomorphic +// mocked DB. The run-ops-id/cuid format + residency are then proven through the SAME isomorphic // helpers the real trigger path uses (`generateRunOpsId` / `RunId.toFriendlyId` / // `RunId.fromFriendlyId` / `ownerEngine`). import type { PrismaClient } from "@trigger.dev/database"; @@ -27,14 +27,14 @@ vi.setConfig({ testTimeout: 60_000 }); // The real trigger-path mint helper, copied verbatim from triggerTask.server.ts so the // test exercises the exact id format a cut-over env produces. -function mintRunKsuidFriendlyId(): string { +function mintRunOpsFriendlyId(): string { return RunId.toFriendlyId(generateRunOpsId()); } -// Mirrors the real trigger path: resolve the kind, then mint either a KSUID friendlyId or +// Mirrors the real trigger path: resolve the kind, then mint either a run-ops friendlyId or // the default cuid one (RunId.generate()). function mintRunFriendlyId(kind: RunIdMintKind): string { - return kind === "ksuid" ? mintRunKsuidFriendlyId() : RunId.generate().friendlyId; + return kind === "ksuid" ? mintRunOpsFriendlyId() : RunId.generate().friendlyId; } async function seedOrgEnv(prisma: PrismaClient, mintFlag?: RunIdMintKind) { @@ -77,9 +77,9 @@ function realFlag(prisma: PrismaClient) { }; } -describe("per-env KSUID mint cutover", () => { +describe("per-env run-ops-id mint cutover", () => { postgresTest( - "canary org mints KSUID/NEW; non-canary org mints cuid/LEGACY", + "canary org mints run-ops/NEW; non-canary org mints cuid/LEGACY", async ({ prisma }) => { const a = await seedOrgEnv(prisma, "ksuid"); // canary const b = await seedOrgEnv(prisma); // not cut over @@ -111,7 +111,7 @@ describe("per-env KSUID mint cutover", () => { ); postgresTest( - "split OFF mints cuid even for a flagged-ksuid org (split gate dominates)", + "split OFF mints cuid even for a 'ksuid'-flagged org (split gate dominates)", async ({ prisma }) => { const a = await seedOrgEnv(prisma, "ksuid"); const flag = vi.fn(realFlag(prisma)); @@ -127,13 +127,13 @@ describe("per-env KSUID mint cutover", () => { ); postgresTest( - "drain-new-forward (D8): flipping back to cuid stops new KSUID mints without reverting existing", + "drain-new-forward (D8): flipping back to cuid stops new run-ops mints without reverting existing", async ({ prisma }) => { const a = await seedOrgEnv(prisma, "ksuid"); const flag = realFlag(prisma); const deps = { masterEnabled: true, splitEnabled: async () => true, flag }; - // First run is born KSUID/NEW while cut over. + // First run is born run-ops/NEW while cut over. const firstKind = await computeRunIdMintKind( { organizationId: a.organization.id, id: a.environment.id }, deps @@ -158,7 +158,7 @@ describe("per-env KSUID mint cutover", () => { expect(nextKind).toBe("cuid"); expect(ownerEngine(RunId.fromFriendlyId(nextFriendly))).toBe("LEGACY"); - // The already-minted KSUID run is untouched — drain-new-forward never reverts it. + // The already-minted run-ops run is untouched — drain-new-forward never reverts it. expect(RunId.fromFriendlyId(firstFriendly).length).toBe(26); expect(ownerEngine(RunId.fromFriendlyId(firstFriendly))).toBe("NEW"); } @@ -184,7 +184,7 @@ describe("per-env KSUID mint cutover", () => { // Observed behavior: the mint decision is resolved per the run's OWN org/env flag — // it does NOT inherit the parent's residency. A child in a non-cut-over org mints cuid - // even when its parent was born KSUID. If children must inherit, that inheritance + // even when its parent was born run-ops. If children must inherit, that inheritance // belongs to the child-trigger path, not this resolver. expect(parentKind).toBe("ksuid"); expect(childKind).toBe("cuid"); diff --git a/apps/webapp/test/runsReplicationInstance.test.ts b/apps/webapp/test/runsReplicationInstance.test.ts index 9dc2dd6a106..ea109c5d3ba 100644 --- a/apps/webapp/test/runsReplicationInstance.test.ts +++ b/apps/webapp/test/runsReplicationInstance.test.ts @@ -153,7 +153,7 @@ describe("assertReplicationCoversSplit (boot gate-coupling)", () => { }; it('throws when split is on but sources[] has no "new" source (the silent under-count)', () => { - // Split on, but the new replication source is forced off — ksuid runs would not + // Split on, but the new replication source is forced off — run-ops runs would not // reach ClickHouse. This is the exact misconfiguration the boot gate must refuse to boot with. const sources = buildReplicationSources({ ...baseArgs, diff --git a/apps/webapp/test/runsRepository.readthrough.test.ts b/apps/webapp/test/runsRepository.readthrough.test.ts index edc8d513fc1..02944506992 100644 --- a/apps/webapp/test/runsRepository.readthrough.test.ts +++ b/apps/webapp/test/runsRepository.readthrough.test.ts @@ -352,11 +352,11 @@ describe("RunsRepository read-through id-set hydrate (PG14 legacy + PG17 new)", } ); - // Full-keyset walk over interleaved cuid + ksuid ids: hydration must preserve the ClickHouse + // Full-keyset walk over interleaved cuid + run-ops ids: hydration must preserve the ClickHouse // (created_at DESC, run_id DESC) order across the id-space seam. A hydrate that reverts to lexical // `id desc` splits the two id-spaces into separate blocks, so it would fail this walk. replicationContainerTest( - "paginating the full keyset enumerates every interleaved cuid/ksuid id once, in CH keyset order, with no empty page", + "paginating the full keyset enumerates every interleaved cuid/run-ops id once, in CH keyset order, with no empty page", async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => { const { clickhouse } = await setupClickhouseReplication({ prisma, @@ -371,17 +371,17 @@ describe("RunsRepository read-through id-set hydrate (PG14 legacy + PG17 new)", // `id desc` groups all "c" ids ahead of all "2" ids; the created_at order below interleaves // them, so the two orders genuinely differ across the seam. const cuid = (n: number) => `c${String(n).padStart(24, "0")}`; - const ksuid = (n: number) => `2${String(n).padStart(23, "0")}01`; + const runOpsId = (n: number) => `2${String(n).padStart(23, "0")}01`; - // created_at DESC order (index 0 = most recent) interleaves the id-spaces: ksuid, cuid, - // ksuid, cuid, ksuid, cuid. + // created_at DESC order (index 0 = most recent) interleaves the id-spaces: run-ops id, cuid, + // run-ops id, cuid, run-ops id, cuid. const now = Date.now(); const seeds = [ - { id: ksuid(6), friendlyId: "run_k6", createdAt: new Date(now - 0 * 60_000) }, + { id: runOpsId(6), friendlyId: "run_k6", createdAt: new Date(now - 0 * 60_000) }, { id: cuid(5), friendlyId: "run_c5", createdAt: new Date(now - 1 * 60_000) }, - { id: ksuid(4), friendlyId: "run_k4", createdAt: new Date(now - 2 * 60_000) }, + { id: runOpsId(4), friendlyId: "run_k4", createdAt: new Date(now - 2 * 60_000) }, { id: cuid(3), friendlyId: "run_c3", createdAt: new Date(now - 3 * 60_000) }, - { id: ksuid(2), friendlyId: "run_k2", createdAt: new Date(now - 4 * 60_000) }, + { id: runOpsId(2), friendlyId: "run_k2", createdAt: new Date(now - 4 * 60_000) }, { id: cuid(1), friendlyId: "run_c1", createdAt: new Date(now - 5 * 60_000) }, ]; for (const s of seeds) { diff --git a/apps/webapp/test/sessions.readthrough.test.ts b/apps/webapp/test/sessions.readthrough.test.ts index 23410c29f6c..d5d5358565c 100644 --- a/apps/webapp/test/sessions.readthrough.test.ts +++ b/apps/webapp/test/sessions.readthrough.test.ts @@ -91,7 +91,7 @@ async function createLegacyRun( } /** - * Create a NEW (dedicated run-ops) TaskRun with a ksuid id — classifies NEW and + * Create a NEW (dedicated run-ops) TaskRun with a run-ops id — classifies NEW and * lives only on the run-ops DB. Scalar tenant columns only (the subset schema is * FK-free, so no org/project/env rows are required here). */ @@ -250,10 +250,10 @@ describe("sessions serializer currentRunId resolution", () => { ); // --- Split single-run across two physical DBs (the production-shaped break) --- - // ksuid (NEW-DB) session run must serialize a non-null friendlyId, and a cuid + // run-ops id (NEW-DB) session run must serialize a non-null friendlyId, and a cuid // (LEGACY) run must still resolve — proving the asymmetry is gone. heteroRunOpsPostgresTest( - "split single-run resolves a NEW-ksuid run from the run-ops DB and a LEGACY-cuid run from control-plane", + "split single-run resolves a NEW-run-ops run from the run-ops DB and a LEGACY-cuid run from control-plane", async ({ prisma14, prisma17 }) => { const ctx = await seedParents(prisma14, "split-single"); diff --git a/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts b/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts index 453468eb768..2bfe3d0571f 100644 --- a/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts +++ b/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts @@ -259,7 +259,7 @@ async function seedOrgProjectEnv(prisma: PrismaClient, suffix: string) { describe("UpdateMetadataService store routing (hetero)", () => { heteroPostgresTest( - "routes read+CAS to the owning (NEW/PG17) store for a KSUID run", + "routes read+CAS to the owning (NEW/PG17) store for a run-ops run", async ({ prisma17, prisma14 }) => { const runId = generateRunOpsId(); expect(runId.length).toBe(26); diff --git a/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts b/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts index d9250496955..d4ea1eba6ab 100644 --- a/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts +++ b/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts @@ -554,7 +554,7 @@ describe("RunOpsCascadeCleanupService", () => { ); // The two-writer split — an env whose rows straddle both DBs (cuid runs on the LEGACY DB, - // ksuid runs on the NEW DB) is fully cleaned by one call; a single-writer service leaks orphans. + // run-ops runs on the NEW DB) is fully cleaned by one call; a single-writer service leaks orphans. heteroPostgresTest( "two-writer fan-out cleans a split env on both DBs; single-writer leaves orphans", async ({ prisma14, prisma17 }) => { diff --git a/apps/webapp/test/waitpointCallback.controlPlane.test.ts b/apps/webapp/test/waitpointCallback.controlPlane.test.ts index 54dc125be61..8fdac127435 100644 --- a/apps/webapp/test/waitpointCallback.controlPlane.test.ts +++ b/apps/webapp/test/waitpointCallback.controlPlane.test.ts @@ -37,7 +37,7 @@ vi.mock("~/db.server", async () => { runOpsNewPrisma: lazyProxy(replicaHolder, "replicaHolder.client"), runOpsNewReplica: lazyProxy(replicaHolder, "replicaHolder.client"), runOpsLegacyReplica: lazyProxy(replicaHolder, "replicaHolder.client"), - // The route's read-through helper reads this off `~/db.server`; split-on routes ksuid (NEW) + // The route's read-through helper reads this off `~/db.server`; split-on routes run-ops id (NEW) // ids to the `runOpsNewReplica` proxy, which points at the seeded container. runOpsSplitReadEnabled: true, sqlDatabaseSchema: Prisma.sql([`public`]), diff --git a/apps/webapp/test/waitpointPresenter.readthrough.test.ts b/apps/webapp/test/waitpointPresenter.readthrough.test.ts index 4cb58092338..a60562a9507 100644 --- a/apps/webapp/test/waitpointPresenter.readthrough.test.ts +++ b/apps/webapp/test/waitpointPresenter.readthrough.test.ts @@ -378,7 +378,7 @@ describe("WaitpointPresenter connected-runs hydrate routed through read-through describe("WaitpointPresenter bare-ctor production default activates readThroughRun", () => { heteroPostgresTest( - "ksuid waitpoint on the new DB resolves via readThroughRun production defaults", + "run-ops waitpoint on the new DB resolves via readThroughRun production defaults", async ({ prisma14, prisma17 }) => { const ctx = await seedParents(prisma17, "proddefault"); const seeded = await seedWaitpoint(prisma17, ctx, "waitpoint_proddefault", { diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 6220fc265a4..1dc7788f3f9 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -2365,7 +2365,7 @@ model ProjectAlert { type ProjectAlertType - // Run-ops split: these reference run-subgraph rows that live on the dedicated run-ops DB for ksuid + // Run-ops split: these reference run-subgraph rows that live on the dedicated run-ops DB for run-ops id // runs, so the cross-DB FK can't hold. Scalar-only; the run is resolved via runStore.findRun. taskRunAttemptId String? diff --git a/internal-packages/run-engine/src/engine/errors.ts b/internal-packages/run-engine/src/engine/errors.ts index 170a9dec5b0..f5ca48160aa 100644 --- a/internal-packages/run-engine/src/engine/errors.ts +++ b/internal-packages/run-engine/src/engine/errors.ts @@ -118,7 +118,7 @@ export class UnclassifiableWaitpointId extends Error { readonly cause?: unknown; constructor(waitpointId: string, options?: { cause?: unknown }) { super( - `Unclassifiable waitpointId for completion: length ${waitpointId.length} matches neither cuid nor ksuid — waitpointId=${JSON.stringify( + `Unclassifiable waitpointId for completion: length ${waitpointId.length} matches neither cuid nor run-ops id — waitpointId=${JSON.stringify( waitpointId )}` ); diff --git a/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts b/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts index f2aa7557fc2..342dee4d419 100644 --- a/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts +++ b/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts @@ -211,7 +211,7 @@ describe("debounceSystem store routing (single-DB passthrough)", () => { // Even on the tx path the snapshot read routes through the store. // getLatestExecutionSnapshot always passes this.$.runStore, so the read is routed to the - // OWNING DB (correct for split mode — a ksuid run's snapshot lives on the dedicated DB, not the + // OWNING DB (correct for split mode — a run-ops run's snapshot lives on the dedicated DB, not the // caller's control-plane tx). Driving the locked reschedule inside a tx must still increment the // counting store's snapshot-read counter. containerTest( diff --git a/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts b/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts index 2e5a7347218..050b3e77fb8 100644 --- a/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts +++ b/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts @@ -1415,7 +1415,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", // ----- Group 5: cross-seam two-store + loud-ambiguity + pinning ----- - // Cross-seam completion applied to the OWNING store: a ksuid waitpoint resides on the dedicated + // Cross-seam completion applied to the OWNING store: a run-ops waitpoint resides on the dedicated // run-ops (NEW) DB, a cuid waitpoint on the legacy/control-plane DB. Driving the completion at the // store seam (forWaitpointCompletion -> updateManyWaitpoints, as the engine does) must apply each // completion to its owning store only. @@ -1429,17 +1429,17 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", const envLegacy = await seedHeteroEnvironment(prisma14, "csl"); const envNew = await seedHeteroEnvironment(prisma17, "csn"); - // v1 body (26 chars, version "1" at index 25) => ksuid => NEW (dedicated run-ops DB); 25-char body => cuid => LEGACY. - const ksuidId = "waitpoint_" + "a".repeat(24) + "01"; + // v1 body (26 chars, version "1" at index 25) => run-ops id => NEW (dedicated run-ops DB); 25-char body => cuid => LEGACY. + const runOpsId = "waitpoint_" + "a".repeat(24) + "01"; const cuidId = "waitpoint_" + "b".repeat(25); await prisma17.waitpoint.create({ data: { - id: ksuidId, + id: runOpsId, friendlyId: "waitpoint_ks", type: "MANUAL", status: "PENDING", - idempotencyKey: `idem_${ksuidId}`, + idempotencyKey: `idem_${runOpsId}`, userProvidedIdempotencyKey: false, projectId: envNew.projectId, environmentId: envNew.id, @@ -1459,9 +1459,9 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", }); const completedAt = new Date(); - const ownerKsuid = await router.forWaitpointCompletion(ksuidId, { routeKind: "MANUAL" }); - await ownerKsuid.updateManyWaitpoints({ - where: { id: ksuidId, status: "PENDING" }, + const runOpsOwner = await router.forWaitpointCompletion(runOpsId, { routeKind: "MANUAL" }); + await runOpsOwner.updateManyWaitpoints({ + where: { id: runOpsId, status: "PENDING" }, data: { status: "COMPLETED", completedAt }, }); const ownerCuid = await router.forWaitpointCompletion(cuidId, { routeKind: "MANUAL" }); @@ -1470,11 +1470,11 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", data: { status: "COMPLETED", completedAt }, }); - // ksuid completed on the dedicated run-ops (NEW) DB only. - expect((await prisma17.waitpoint.findUniqueOrThrow({ where: { id: ksuidId } })).status).toBe( + // run-ops id completed on the dedicated run-ops (NEW) DB only. + expect((await prisma17.waitpoint.findUniqueOrThrow({ where: { id: runOpsId } })).status).toBe( "COMPLETED" ); - expect(await prisma14.waitpoint.findUnique({ where: { id: ksuidId } })).toBeNull(); + expect(await prisma14.waitpoint.findUnique({ where: { id: runOpsId } })).toBeNull(); // cuid completed on the legacy DB only. expect((await prisma14.waitpoint.findUniqueOrThrow({ where: { id: cuidId } })).status).toBe( "COMPLETED" @@ -1484,7 +1484,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", ); // Ambiguity resolution: forWaitpointCompletion safe-classifies an id matching neither cuid nor - // ksuid to LEGACY, then probes both DBs. With no row anywhere it resolves to the LEGACY fallback + // run-ops id to LEGACY, then probes both DBs. With no row anywhere it resolves to the LEGACY fallback // rather than throwing — the loud-failure contract lives at the engine seam (completeWaitpoint // re-reads and surfaces "Waitpoint not found"). The residency probe made this method async. heteroPostgresTest( @@ -1500,7 +1500,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", } ); - // Pinning proof: a cross-tree-idempotency completion of a ksuid + // Pinning proof: a cross-tree-idempotency completion of a run-ops id // (NEW residency) waitpoint pins to the LEGACY store. heteroPostgresTest( "cross-seam cross-tree-idempotency completion pins to legacy", @@ -1510,8 +1510,8 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", const router = new RoutingRunStore({ new: newStore, legacy }); // pin is DRIVEN via explicit context at the store seam; the engine completeWaitpoint entry cannot derive it — the organic cross-tree-idempotency pin is applied at the webapp idempotency caller. - const ksuidId = "waitpoint_" + "a".repeat(24) + "01"; - const handle = await router.forWaitpointCompletion(ksuidId, { + const runOpsId = "waitpoint_" + "a".repeat(24) + "01"; + const handle = await router.forWaitpointCompletion(runOpsId, { routeKind: "IDEMPOTENCY_REUSE", isCrossTreeIdempotency: true, }); diff --git a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts index 6687cdb9071..344a0c1d8c5 100644 --- a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts @@ -229,7 +229,7 @@ export class WaitpointSystem { // Co-location invariant: a DATETIME wait waitpoint lives on the same run-ops DB as the run that // blocks on it (so the block edge's local `Waitpoint` join resolves and completion/resume stay // local). The minted waitpoint id is always a cuid, so without `coLocateWithRunId` the upsert - // would always route to LEGACY and a ksuid run on NEW would hang. The (env,idempotencyKey) dedup + // would always route to LEGACY and a run-ops run on NEW would hang. The (env,idempotencyKey) dedup // is within the owning run/tree (co-resident on one DB), so the dedup probe + rotation target the // SAME store. With no run id (a standalone token has no owning run yet) the lookup falls back to // a cross-DB NEW-then-LEGACY scan and the upsert routes by id-shape. A caller-supplied tx pins a diff --git a/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts b/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts index eabc9577a6f..e930b1cfd58 100644 --- a/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts +++ b/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts @@ -1,6 +1,6 @@ // Block-edge write goes to the wrong DB so the parent never suspends. Two-physical-DB topology with // the real dedicated run-ops schema on #new (prisma17). RED before the fix: the control-plane tx -// threaded by RunEngine.trigger forces the raw CTE to join `Waitpoint` on #legacy, where the ksuid +// threaded by RunEngine.trigger forces the raw CTE to join `Waitpoint` on #legacy, where the run-ops id // waitpoint does not exist, so 0 edges are written and the parent stays EXECUTING. GREEN after: the // block path always routes through the store, landing the edge + WaitpointRunConnection on #new and // suspending the parent. (Snapshot reads/writes route by run id regardless of tx.) @@ -29,9 +29,9 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. -const KSUID_A = "k".repeat(24) + "01"; -const KSUID_B = "m".repeat(24) + "01"; +// run-ops id (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. +const RUN_OPS_A = "k".repeat(24) + "01"; +const RUN_OPS_B = "m".repeat(24) + "01"; function baseEngineOptions(redisOptions: any, prisma: any) { return { @@ -128,9 +128,9 @@ function buildCreateRunInput(params: { }; } -// Seed an EXECUTING ksuid parent run on #new (prisma17) via the routed store, then a ksuid PENDING +// Seed an EXECUTING run-ops parent run on #new (prisma17) via the routed store, then a run-ops id PENDING // RUN waitpoint co-resident on #new. Returns the env + ids the block path needs. -async function seedExecutingKsuidParent( +async function seedExecutingRunOpsParent( prisma14: PrismaClient, prisma17: RunOpsPrismaClient, router: RoutingRunStore, @@ -166,7 +166,7 @@ async function seedExecutingKsuidParent( prisma14 ); - // The associated waitpoint lives on #new (co-resident with the ksuid run). + // The associated waitpoint lives on #new (co-resident with the run-ops run). await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -198,11 +198,11 @@ function makeRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { } describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () => { - // RED before fix / GREEN after: a ksuid parent blocked by a #new-resident waitpoint, with the + // RED before fix / GREEN after: a run-ops parent blocked by a #new-resident waitpoint, with the // control-plane tx threaded exactly as RunEngine.trigger does, ends EXECUTING_WITH_WAITPOINTS with // the edge + WaitpointRunConnection physically on #new. twoDbEngineTest( - "blockRunWithWaitpoint suspends a ksuid parent with the edge on #new (control-plane tx threaded)", + "blockRunWithWaitpoint suspends a run-ops parent with the edge on #new (control-plane tx threaded)", async ({ prisma14, prisma17, redisOptions }) => { const router = makeRouter(prisma14 as unknown as PrismaClient, prisma17); const engine = new RunEngine({ @@ -211,9 +211,9 @@ describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () }); try { - const parentRunId = `run_${KSUID_A}`; - const waitpointId = `waitpoint_${KSUID_A}`; - const env = await seedExecutingKsuidParent( + const parentRunId = `run_${RUN_OPS_A}`; + const waitpointId = `waitpoint_${RUN_OPS_A}`; + const env = await seedExecutingRunOpsParent( prisma14 as unknown as PrismaClient, prisma17, router, @@ -267,9 +267,9 @@ describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () }); try { - const parentRunId = `run_${KSUID_B}`; - const waitpointId = `waitpoint_${KSUID_B}`; - const env = await seedExecutingKsuidParent( + const parentRunId = `run_${RUN_OPS_B}`; + const waitpointId = `waitpoint_${RUN_OPS_B}`; + const env = await seedExecutingRunOpsParent( prisma14 as unknown as PrismaClient, prisma17, router, @@ -283,7 +283,7 @@ describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () runId: parentRunId, waitpoints: waitpointId, projectId: env.project.id, - batch: { id: `batch_${KSUID_B}`, index: 0 }, + batch: { id: `batch_${RUN_OPS_B}`, index: 0 }, tx: prisma14 as unknown as PrismaClient, }); diff --git a/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts b/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts index d5df10dba4b..251d556d960 100644 --- a/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts +++ b/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts @@ -3,7 +3,7 @@ // control-plane client. Two-physical-DB topology with the real dedicated run-ops schema on // #new (prisma17), modelled on the block-edge residency test. // -// RED before the fix: completeWaitpoint resolved the #new store (where the ksuid RUN waitpoint +// RED before the fix: completeWaitpoint resolved the #new store (where the run-ops id RUN waitpoint // lives), marked it COMPLETED there, then re-read it via `store.findWaitpoint({where:{id}}, this.$.prisma)`. // A resolved PostgresRunStore HONORS the passed client, so the re-read hit the control-plane DB // (#legacy / prisma14), found nothing, and threw "Waitpoint not found" BEFORE enqueueing @@ -35,10 +35,10 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. -const KSUID_A = "n".repeat(24) + "01"; -// A second ksuid run for the cross-DB (NEW-run → LEGACY-token) case. -const KSUID_X = "k".repeat(24) + "01"; +// run-ops id (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. +const RUN_OPS_A = "n".repeat(24) + "01"; +// A second run-ops run for the cross-DB (NEW-run → LEGACY-token) case. +const RUN_OPS_X = "k".repeat(24) + "01"; // cuid (25-char) → classified LEGACY → a standalone token resident on #legacy (prisma14). const CUID_25 = "c".repeat(25); @@ -134,9 +134,9 @@ function buildCreateRunInput(params: { }; } -// Seed an EXECUTING ksuid parent on #new (prisma17) via the routed store, plus a ksuid PENDING RUN +// Seed an EXECUTING run-ops parent on #new (prisma17) via the routed store, plus a run-ops id PENDING RUN // waitpoint co-resident on #new. Returns the env + ids the block/complete path needs. -async function seedExecutingKsuidParent( +async function seedExecutingRunOpsParent( prisma14: PrismaClient, prisma17: RunOpsPrismaClient, router: RoutingRunStore, @@ -170,7 +170,7 @@ async function seedExecutingKsuidParent( prisma14 ); - // The RUN waitpoint lives on #new, co-resident with the ksuid run, and is completed-by that run. + // The RUN waitpoint lives on #new, co-resident with the run-ops run, and is completed-by that run. await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -188,10 +188,10 @@ async function seedExecutingKsuidParent( return env; } -// Seed an EXECUTING ksuid parent on #new (prisma17) AND a standalone MANUAL token resident on +// Seed an EXECUTING run-ops parent on #new (prisma17) AND a standalone MANUAL token resident on // #legacy (prisma14, cuid) — the tolerated NEW-run → LEGACY-token cross-DB direction (standalone // tokens are minted on LEGACY). The token is NOT created on #new. Returns both envs + ids. -async function seedKsuidParentAndLegacyToken( +async function seedRunOpsParentAndLegacyToken( prisma14: PrismaClient, prisma17: RunOpsPrismaClient, router: RoutingRunStore, @@ -267,9 +267,9 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic }); try { - const parentRunId = `run_${KSUID_A}`; - const waitpointId = `waitpoint_${KSUID_A}`; - const env = await seedExecutingKsuidParent( + const parentRunId = `run_${RUN_OPS_A}`; + const waitpointId = `waitpoint_${RUN_OPS_A}`; + const env = await seedExecutingRunOpsParent( prisma14 as unknown as PrismaClient, prisma17, router, @@ -335,7 +335,7 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic } ); - // End-to-end cross-DB gate: a ksuid run on #new blocked on a standalone MANUAL token resident on + // End-to-end cross-DB gate: a run-ops run on #new blocked on a standalone MANUAL token resident on // #legacy (the tolerated NEW-run → LEGACY-token direction — standalone tokens are minted on // LEGACY). RED before the writer fix: blockRunWithWaitpointEdges' dedicated branch joined // `FROM "Waitpoint" w`, which matched 0 rows on #new (the token is on #legacy) → 0 edges → the run @@ -344,7 +344,7 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic // token (the completion fan-out discovers the #new edge and resolves its COMPLETED status across // both DBs) resumes the NEW run. twoDbEngineTest( - "completeWaitpoint on a LEGACY-resident token unblocks a ksuid run whose edge lives on #new", + "completeWaitpoint on a LEGACY-resident token unblocks a run-ops run whose edge lives on #new", async ({ prisma14, prisma17, redisOptions }) => { const router = makeRouter(prisma14 as unknown as PrismaClient, prisma17); const engine = new RunEngine({ @@ -353,9 +353,9 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic }); try { - const parentRunId = `run_${KSUID_X}`; // ksuid run → #new + const parentRunId = `run_${RUN_OPS_X}`; // run-ops run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid standalone token → #legacy - const env = await seedKsuidParentAndLegacyToken( + const env = await seedRunOpsParentAndLegacyToken( prisma14 as unknown as PrismaClient, prisma17, router, @@ -374,7 +374,7 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic tx: prisma14 as unknown as PrismaClient, }); - // The block edge is physically on #new; #legacy holds none for the ksuid run (safety invariant). + // The block edge is physically on #new; #legacy holds none for the run-ops run (safety invariant). expect(await prisma17.taskRunWaitpoint.count({ where: { taskRunId: parentRunId } })).toBe( 1 ); diff --git a/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts b/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts index 7030c0e43ff..ac47e75296f 100644 --- a/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts +++ b/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts @@ -1,9 +1,9 @@ // DATETIME / MANUAL waitpoint co-location with the owning run (run-ops split). // // The bug: `wait.for`/`wait.until` (DATETIME) and wait-token (MANUAL) waitpoints over the ~5s -// checkpoint threshold hang a ksuid run forever. `createDateTimeWaitpoint`/`createManualWaitpoint` +// checkpoint threshold hang a run-ops run forever. `createDateTimeWaitpoint`/`createManualWaitpoint` // mint an ALWAYS-cuid WaitpointId, and the routing store routed the upsert by that id → #legacy, -// even though the owning ksuid run lives on #new. `blockRunWithWaitpoint` then writes its block edge +// even though the owning run-ops run lives on #new. `blockRunWithWaitpoint` then writes its block edge // on #new (routed by run id), but the CTE joins `Waitpoint` LOCALLY on #new — where the // waitpoint does not exist — so it writes 0 edges and the run is never actually blocked nor resumed. // @@ -35,11 +35,11 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. -const KSUID_A = "k".repeat(24) + "01"; -const KSUID_B = "m".repeat(24) + "01"; -const KSUID_C = "n".repeat(24) + "01"; -const KSUID_D = "p".repeat(24) + "01"; +// run-ops id (v1 internal id, version "1" at index 25) → classified NEW → routed to the run-ops (#new) store. +const RUN_OPS_A = "k".repeat(24) + "01"; +const RUN_OPS_B = "m".repeat(24) + "01"; +const RUN_OPS_C = "n".repeat(24) + "01"; +const RUN_OPS_D = "p".repeat(24) + "01"; function baseEngineOptions(redisOptions: any, prisma: any) { return { @@ -133,8 +133,8 @@ function buildCreateRunInput(params: { }; } -// Seed an EXECUTING ksuid run on #new (prisma17) via the routed store. Returns the env + run id. -async function seedExecutingKsuidRun( +// Seed an EXECUTING run-ops run on #new (prisma17) via the routed store. Returns the env + run id. +async function seedExecutingRunOpsRun( prisma14: PrismaClient, router: RoutingRunStore, runId: string, @@ -184,7 +184,7 @@ function makeRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { } describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physical DBs)", () => { - // RED before fix: the DATETIME waitpoint created for a ksuid run lands on #legacy (routed by its + // RED before fix: the DATETIME waitpoint created for a run-ops run lands on #legacy (routed by its // own cuid id), so the block edge (on #new) finds no local waitpoint and the run never blocks/resumes. // GREEN after: the waitpoint co-locates on #new, the edge resolves, and the run resumes once the // datetime waitpoint completes via the engine's finishWaitpoint timer. @@ -196,8 +196,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_A}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "dta"); + const runId = `run_${RUN_OPS_A}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "dta"); // ~600ms out so the finishWaitpoint timer fires within the test window. const date = new Date(Date.now() + 600); @@ -255,8 +255,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_B}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "mna"); + const runId = `run_${RUN_OPS_B}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "mna"); const { waitpoint } = await engine.createManualWaitpoint({ runId, @@ -310,8 +310,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_C}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "idem"); + const runId = `run_${RUN_OPS_C}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "idem"); const idempotencyKey = "dedup-key-1"; const date = new Date(Date.now() + 60_000); @@ -364,8 +364,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_D}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "idemm"); + const runId = `run_${RUN_OPS_D}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "idemm"); const idempotencyKey = "dedup-key-2"; const first = await engine.createManualWaitpoint({ diff --git a/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts b/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts index 08a3cf2260f..cd24ba1255a 100644 --- a/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts +++ b/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts @@ -452,16 +452,20 @@ describe("RunEngine lifecycle read routing (single-DB)", () => { // Read-through / cross-version proofs (PG14 legacy <-> PG17 run-ops). These test // the routing layer the engine's threaded reads delegate to: a real RoutingRunStore // over two real PostgresRunStores on two real containers (NEVER mocked). A new run -// (ksuid id, born on PG17) resolves from the run-ops store; an old in-retention run +// (run-ops id, born on PG17) resolves from the run-ops store; an old in-retention run // (cuid id, on PG14) reads THROUGH the legacy store's read-only (replica) client. // --------------------------------------------------------------------------- // A cuid-length (25-char) internal id → classifies LEGACY; a v1-shaped (26-char, version "1") // internal id → classifies NEW. The `run_` prefix is stripped before classification. const legacyRunId = (suffix: string) => `run_${suffix.padEnd(25, "0").slice(0, 25)}`; +// Map each suffix char into the base32hex alphabet by code point (not a lossy outlier→"0" replace, +// which collapsed suffixes differing only in out-of-range chars): 24-char core + region + version. +const BASE32HEX = "0123456789abcdefghijklmnopqrstuv"; const newRunId = (suffix: string) => - `run_${suffix - .replace(/[^0-9a-v]/g, "0") + `run_${[...suffix] + .map((ch) => BASE32HEX[ch.charCodeAt(0) % 32]) + .join("") .padEnd(24, "0") .slice(0, 24)}01`; @@ -531,7 +535,7 @@ async function seedRunWithSnapshot( } describe("RunEngine lifecycle read-through routing (PG14/PG17)", () => { - // A NEW run (ksuid id) seeded only on the run-ops (PG17/new) store resolves + // A NEW run (run-ops id) seeded only on the run-ops (PG17/new) store resolves // its latest snapshot from that store, and the legacy store is never touched. heteroPostgresTest( "a new run resolves its latest snapshot from the run-ops store", diff --git a/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts b/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts index ea7bbeb263d..34353cb44e0 100644 --- a/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts +++ b/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts @@ -100,7 +100,7 @@ function freshRunId() { return RunId.generate().friendlyId; } -function freshKsuidRunId() { +function freshRunOpsRunId() { return RunId.toFriendlyId(generateRunOpsId()); } @@ -381,7 +381,7 @@ describe("RunEngine trigger/create routing", () => { } ); - // Split/two-store proof: with the ksuid mint enabled, a NEW-minted run id is + // Split/two-store proof: with the run-ops id mint enabled, a NEW-minted run id is // classified NEW and a RoutingRunStore writes it to the run-ops (NEW) store, // never the LEGACY store. Proves a new run is born on the run-ops store. containerTest( @@ -405,7 +405,7 @@ describe("RunEngine trigger/create routing", () => { const taskIdentifier = "test-task"; await setupBackgroundWorker(engine, environment, taskIdentifier); - const friendlyId = freshKsuidRunId(); + const friendlyId = freshRunOpsRunId(); // Sanity: this id classifies NEW so RoutingRunStore must pick newStore. expect(ownerEngine(friendlyId)).toBe("NEW"); @@ -426,7 +426,7 @@ describe("RunEngine trigger/create routing", () => { // A child triggered with the parent's residency persists to the // SAME store the parent was written to (routing-by-run-id). Both parent and - // child mint NEW (ksuid) ids → both land on newStore. + // child mint NEW (run-ops id) ids → both land on newStore. containerTest("child inherits the parent's residency store", async ({ prisma, redisOptions }) => { const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); const newStore = new CountingRunStore({ prisma, readOnlyPrisma: prisma, label: "new" }); @@ -448,7 +448,7 @@ describe("RunEngine trigger/create routing", () => { await setupBackgroundWorker(engine, environment, [parentTask, childTask]); const parentRun = await engine.trigger( - baseTriggerParams(freshKsuidRunId(), environment, parentTask) + baseTriggerParams(freshRunOpsRunId(), environment, parentTask) ); await engine.dequeueFromWorkerQueue({ consumerId: "test", workerQueue: "main" }); @@ -459,7 +459,7 @@ describe("RunEngine trigger/create routing", () => { }); const childRun = await engine.trigger({ - ...baseTriggerParams(freshKsuidRunId(), environment, childTask), + ...baseTriggerParams(freshRunOpsRunId(), environment, childTask), resumeParentOnCompletion: true, parentTaskRunId: parentRun.id, rootTaskRunId: parentRun.id, diff --git a/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts b/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts index 4c2e1ea7ad6..54e76feba44 100644 --- a/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts @@ -1,13 +1,13 @@ // RED→GREEN repro for the run-ops split BASELINE BLOCKER: // RoutingRunStore cross-DB PROBE reads forward the caller's control-plane `client` into the #new // sub-store probe, so #new queries the CONTROL-PLANE DB instead of its own (5434) and never finds a -// ksuid-resident batch/attempt → returns null. Live effect: batchSystem.#tryCompleteBatch calls +// run-ops id-resident batch/attempt → returns null. Live effect: batchSystem.#tryCompleteBatch calls // `runStore.findBatchTaskRunById(batchId, undefined, this.$.prisma)` → null → "batch doesn't exist" // → the batch waitpoint is never completed → every `batchTriggerAndWait` parent hangs forever. // // `heteroRunOpsPostgresTest` gives a REAL split topology: prisma17 = real RunOpsPrismaClient over the // dedicated subset schema (#new / 5434), prisma14 = full legacy schema on a SEPARATE physical PG -// container (#legacy / control-plane). NEVER mocked. The repro seeds a ksuid batch (and a ksuid +// container (#legacy / control-plane). NEVER mocked. The repro seeds a run-ops batch (and a run-ops id // attempt) on #new and probes via the router passing the LEGACY client as the read client — exactly // as the live caller does. RED before the fix (router forwards the client → #new reads control-plane // → null); GREEN after (router drops the client → #new reads its own DB → finds the row). @@ -96,13 +96,13 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c // findBatchTaskRunById — the live batchTriggerAndWait hang: #tryCompleteBatch probes with the // control-plane client, which the router forwarded into #new → #new read the wrong DB → null. heteroRunOpsPostgresTest( - "findBatchTaskRunById FINDS a ksuid batch on #new even when probed with the LEGACY (control-plane) client", + "findBatchTaskRunById FINDS a run-ops batch on #new even when probed with the LEGACY (control-plane) client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchprobe_new"); - const batchId = `batch_${NEW_ID_26}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id → #new - // Seed the batch directly on #new (5434), exactly where a runEngine-routed ksuid batch lives. + // Seed the batch directly on #new (5434), exactly where a runEngine-routed run-ops batch lives. await prisma17.batchTaskRun.create({ data: { id: batchId, @@ -151,7 +151,7 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c // findBatchTaskRunByFriendlyId — same anti-pattern (env-scoped friendlyId probe). heteroRunOpsPostgresTest( - "findBatchTaskRunByFriendlyId FINDS a ksuid batch on #new despite the LEGACY client", + "findBatchTaskRunByFriendlyId FINDS a run-ops batch on #new despite the LEGACY client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchfid_new"); @@ -180,7 +180,7 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c // findBatchTaskRunByIdempotencyKey — same anti-pattern (env + idempotency-key probe). heteroRunOpsPostgresTest( - "findBatchTaskRunByIdempotencyKey FINDS a ksuid batch on #new despite the LEGACY client", + "findBatchTaskRunByIdempotencyKey FINDS a run-ops batch on #new despite the LEGACY client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchidem_new"); @@ -209,13 +209,13 @@ describe("run-ops split — cross-DB probe reads must NOT forward the caller's c ); // findTaskRunAttempt — same anti-pattern. A classifiable taskRunId routes to the owning store - // (#new for a ksuid run) but the control-plane client was still forwarded into it. + // (#new for a run-ops run) but the control-plane client was still forwarded into it. heteroRunOpsPostgresTest( - "findTaskRunAttempt FINDS a ksuid attempt on #new even when probed with the LEGACY client", + "findTaskRunAttempt FINDS a run-ops id attempt on #new even when probed with the LEGACY client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "attempt_new"); - const runId = `run_${NEW_ID_26}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // run-ops run → #new const attemptId = `attempt_${NEW_ID_26}`; // The attempt's owning run lives on #new (the FK is co-resident on the dedicated schema). diff --git a/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts b/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts index d1a4c5e9ba3..12a438870a8 100644 --- a/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts @@ -1,5 +1,5 @@ -// ProjectAlert.taskRunId/taskRunAttemptId FKs point INTO the run subgraph. A ksuid run lives ONLY -// on the dedicated run-ops DB (prisma17), so `projectAlert.create({ taskRunId: })` on +// ProjectAlert.taskRunId/taskRunAttemptId FKs point INTO the run subgraph. A run-ops run lives ONLY +// on the dedicated run-ops DB (prisma17), so `projectAlert.create({ taskRunId: })` on // control-plane (prisma14) violates the FK and the alert is silently dropped. After the FK drop + // @relation removal the create succeeds; the read path resolves the run via runStore.findRun. // Asserts the create succeeds: it fails with an FK violation before the fix and succeeds after. @@ -50,9 +50,9 @@ async function seedControlPlaneAlertPrereqs(prisma: PrismaClient, suffix: string describe("ProjectAlert control-plane → run-subgraph FK reconciliation", () => { heteroRunOpsPostgresTest( - "creating a TASK_RUN alert with a ksuid taskRunId (run only on the run-ops DB) succeeds on control-plane", + "creating a TASK_RUN alert with a run-ops id taskRunId (run only on the run-ops DB) succeeds on control-plane", async ({ prisma14, prisma17 }) => { - const suffix = "alert-ksuid"; + const suffix = "alert-runops"; const { project, environment, channel } = await seedControlPlaneAlertPrereqs( prisma14, suffix @@ -103,15 +103,15 @@ describe("ProjectAlert control-plane → run-subgraph FK reconciliation", () => ); heteroRunOpsPostgresTest( - "creating a TASK_RUN_ATTEMPT alert with a ksuid taskRunAttemptId (attempt only on the run-ops DB) succeeds on control-plane", + "creating a TASK_RUN_ATTEMPT alert with a run-ops id taskRunAttemptId (attempt only on the run-ops DB) succeeds on control-plane", async ({ prisma14 }) => { - const suffix = "alert-ksuid-attempt"; + const suffix = "alert-run-ops id-attempt"; const { project, environment, channel } = await seedControlPlaneAlertPrereqs( prisma14, suffix ); - // A ksuid attempt id with no matching control-plane TaskRunAttempt row. With the FK present + // A run-ops id attempt id with no matching control-plane TaskRunAttempt row. With the FK present // this throws P2003; after the FK is dropped it succeeds. const attemptId = "a".repeat(24) + "01"; const alert = await prisma14.projectAlert.create({ diff --git a/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts b/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts index 9eb310d4118..17f1e20e605 100644 --- a/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts @@ -4,7 +4,7 @@ // dedicated subset schema (`heteroRunOpsPostgresTest.prisma17` is a real `RunOpsPrismaClient` over // the @internal/run-ops-database SUBSET schema) and the full legacy schema on a SEPARATE physical // PG container (`prisma14`). An earlier harness masked every one of these by backing the "#new" -// store with the FULL legacy schema and globally minting ksuid, so the split never ran against the +// store with the FULL legacy schema and globally minting run-ops id, so the split never ran against the // dedicated schema. // // Each case either asserts the fixed behavior directly or, for a still-open item, wraps the broken @@ -22,7 +22,7 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH (runOpsResidency.ts): 25 chars → cuid → LEGACY, -// a v1 body (version "1" at index 25) → ksuid → NEW. A `run_`-prefixed friendly id strips the first underscore first. +// a v1 body (version "1" at index 25) → run-ops id → NEW. A `run_`-prefixed friendly id strips the first underscore first. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / control-plane DB, full schema) const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / dedicated run-ops DB, subset schema) @@ -248,16 +248,16 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // =========================================================================================== // Cross-DB waitpoint hydration through the router. - // A ksuid run (on #new) blocked by a waitpoint that lives on the OTHER DB (#legacy). The block + // A run-ops run (on #new) blocked by a waitpoint that lives on the OTHER DB (#legacy). The block // edge co-resides with the run on #new; the token is on #legacy. A single store hydrates the // edge's `waitpoint` from its own client → null → the run hangs / loses output. The // router must re-resolve the token across BOTH DBs. // =========================================================================================== - // Co-resident control (the ksuid happy path): a ksuid run blocked by a ksuid waitpoint, + // Co-resident control (the run-ops id happy path): a run-ops run blocked by a run-ops waitpoint, // both on #new, hydrates through the router with the real status/output. heteroRunOpsPostgresTest( - "cross-DB: a ksuid run blocked by a CO-RESIDENT ksuid waitpoint hydrates the real status via the router", + "cross-DB: a run-ops run blocked by a CO-RESIDENT run-ops waitpoint hydrates the real status via the router", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cores_new"); @@ -297,11 +297,11 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche ); // The cross-DB topology. The block edge is on #new (co-resident with the - // ksuid run), the completing token is on #legacy. The router resolves the token across both DBs + // run-ops run), the completing token is on #legacy. The router resolves the token across both DBs // and returns its REAL status and OUTPUT (the wrong-result guard) — a single store would // hydrate null here and strand the run. heteroRunOpsPostgresTest( - "cross-DB: a ksuid run completed by a waitpoint on the OTHER DB hydrates the real status + output via the router", + "cross-DB: a run-ops run completed by a waitpoint on the OTHER DB hydrates the real status + output via the router", async ({ prisma14, prisma17 }) => { const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "xdb_new"); @@ -309,7 +309,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; // cuid → lives on #legacy - // The completing token lives on #legacy (cuid MANUAL token blocking a ksuid run). + // The completing token lives on #legacy (cuid MANUAL token blocking a run-ops run). await prisma14.waitpoint.create({ data: { id: waitpointId, @@ -323,7 +323,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche environmentId: legEnv.environment.id, }, }); - // The block edge co-resides with the ksuid RUN on #new. + // The block edge co-resides with the run-ops id RUN on #new. await prisma17.taskRunWaitpoint.create({ data: { taskRunId: runId, waitpointId, projectId: newEnv.project.id }, }); @@ -362,7 +362,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "phantom_new"); const runId = `run_${NEW_ID_26}`; - const waitpointId = `waitpoint_${"p".repeat(24) + "01"}`; // ksuid-shaped, but never created anywhere + const waitpointId = `waitpoint_${"p".repeat(24) + "01"}`; // run-ops-shaped, but never created anywhere await prisma17.taskRunWaitpoint.create({ data: { taskRunId: runId, waitpointId, projectId: newEnv.project.id }, @@ -471,9 +471,9 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche } ); - // Control: a ksuid run's checkpoint, routed by its owning run id, co-resides on #new. + // Control: a run-ops run's checkpoint, routed by its owning run id, co-resides on #new. heteroRunOpsPostgresTest( - "control: a ksuid run's checkpoint co-resides on #new with its snapshot", + "control: a run-ops run's checkpoint co-resides on #new with its snapshot", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap2k_new"); @@ -493,7 +493,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche data: { friendlyId: `checkpoint_${NEW_ID_26}`, type: "DOCKER", - location: "s3://bucket/ksuid-run-checkpoint", + location: "s3://bucket/run-ops id-run-checkpoint", projectId: env.project.id, runtimeEnvironmentId: env.environment.id, }, @@ -506,7 +506,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const snapshot = await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, - snapshot: { executionStatus: "SUSPENDED", description: "ksuid suspended" }, + snapshot: { executionStatus: "SUSPENDED", description: "run-ops suspended" }, checkpointId: checkpoint.id, environmentId: env.environment.id, environmentType: "DEVELOPMENT", @@ -637,10 +637,10 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche } ); - // Control: a KSUID run (on #new / dedicated) IS visible through the router — proving the read gap + // Control: a run-ops run (on #new / dedicated) IS visible through the router — proving the read gap // is residency-specific (only the cuid/#legacy cohort would be dropped), not a blanket failure. heteroRunOpsPostgresTest( - "control: a ksuid run's #new snapshot IS found through the router", + "control: a run-ops run's #new snapshot IS found through the router", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); @@ -657,7 +657,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche ); const created = await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, - snapshot: { executionStatus: "EXECUTING", description: "ksuid run executing" }, + snapshot: { executionStatus: "EXECUTING", description: "run-ops run executing" }, environmentId: env.environment.id, environmentType: "DEVELOPMENT", projectId: env.project.id, @@ -696,7 +696,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche heteroRunOpsPostgresTest( "mechanism: the block-edge CTE writes ZERO edges when the waitpoint is on the other DB", async ({ prisma14, prisma17 }) => { - // A ksuid parent run + its associated waitpoint live on #new (prisma17 / dedicated). + // A run-ops parent run + its associated waitpoint live on #new (prisma17 / dedicated). const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3_new"); const parentRunId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${NEW_ID_26}`; @@ -826,20 +826,20 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // =========================================================================================== // Lazy RUN-waitpoint residency split. // `getOrCreateRunWaitpoint` creates the lazy RUN waitpoint via `createWaitpoint` - // carrying `completedByTaskRunId: runId`. Production never mints ksuid waitpoint ids, so routing by - // the waitpoint's OWN id-shape would land it on #legacy while a ksuid run is on #new → run-completion + // carrying `completedByTaskRunId: runId`. Production never mints run-ops waitpoint ids, so routing by + // the waitpoint's OWN id-shape would land it on #legacy while a run-ops run is on #new → run-completion // hydrate (associatedWaitpoint by completedByTaskRunId on the run's DB) misses it → parent hangs. // Fix: route the create by the OWNING run id (completedByTaskRunId) so it co-resides with the run. // =========================================================================================== - // A ksuid run's lazy RUN-waitpoint with a CUID-shaped waitpoint id (production-like: ksuid + // A run-ops run's lazy RUN-waitpoint with a CUID-shaped waitpoint id (production-like: run-ops id // waitpoint minting is off) co-resides on #new with the run, NOT on #legacy by its own id-shape. heteroRunOpsPostgresTest( - "a ksuid run's lazy RUN-waitpoint co-resides on #new (routed by completedByTaskRunId)", + "a run-ops run's lazy RUN-waitpoint co-resides on #new (routed by completedByTaskRunId)", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap6_new"); - const runId = `run_${NEW_ID_26}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // run-ops run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid waitpoint id → would route to #legacy by id-shape await router.createRun( @@ -867,7 +867,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche }, }); - // Co-resides with the ksuid run on #new (NOT stranded on #legacy by the cuid id-shape). + // Co-resides with the run-ops run on #new (NOT stranded on #legacy by the cuid id-shape). const onNew = await prisma17.waitpoint.findUnique({ where: { id: waitpointId } }); expect(onNew).not.toBeNull(); const onLegacy = await prisma14.waitpoint.findUnique({ where: { id: waitpointId } }); @@ -887,7 +887,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma14, "legacy", "gap6c_leg"); const runId = `run_${CUID_25}`; // cuid run → #legacy - const waitpointId = `waitpoint_${NEW_ID_26}`; // ksuid waitpoint id → would route to #new by id-shape + const waitpointId = `waitpoint_${NEW_ID_26}`; // run-ops waitpoint id → would route to #new by id-shape await router.createRun( buildCreateRunInput({ @@ -922,13 +922,13 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // =========================================================================================== // Snapshot resume payload must not lose a cross-DB waitpoint's OUTPUT. // `findLatestExecutionSnapshot` hydrates `completedWaitpoints` from the - // snapshot's own (run's) client. A ksuid run resumed by a waitpoint that completed on the OTHER DB + // snapshot's own (run's) client. A run-ops run resumed by a waitpoint that completed on the OTHER DB // (cuid token) would get the token hydrated to a stale/absent row → its OUTPUT silently vanishes from // the resume payload (a wrong-result, not just a wrong dashboard). Fix: the router re-resolves the // snapshot's completed waitpoints across BOTH DBs. // =========================================================================================== - // A ksuid run's latest snapshot lists a completed waitpoint that lives on #legacy + // A run-ops run's latest snapshot lists a completed waitpoint that lives on #legacy // (cross-DB). The single #new store hydrates it null; the router recovers its real OUTPUT. heteroRunOpsPostgresTest( "findLatestExecutionSnapshot recovers a cross-DB completed waitpoint's OUTPUT via the router", @@ -936,7 +936,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "cg1_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "cg1_leg"); - const runId = `run_${NEW_ID_26}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // run-ops run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid token → completed on #legacy await router.createRun( @@ -964,7 +964,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche }, }); - // The latest snapshot (on #new, co-resident with the ksuid run) lists the cross-DB token as a + // The latest snapshot (on #new, co-resident with the run-ops run) lists the cross-DB token as a // completed waitpoint via the CompletedWaitpoint join. await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, @@ -994,7 +994,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // =========================================================================================== // Block-edge WRITER must not require a LOCAL waitpoint row. // The design routes the block edge to the RUN's DB and mints standalone tokens on LEGACY, so a - // ksuid run on #new can legitimately block on a cuid token resident on #legacy (the one tolerated + // run-ops run on #new can legitimately block on a cuid token resident on #legacy (the one tolerated // cross-DB direction — the #new `TaskRunWaitpoint` is FK-free precisely for this). If // `blockRunWithWaitpointEdges`'s dedicated branch sourced the edge rows from // `FROM "Waitpoint" w WHERE w.id = ANY(...)`, then when the token is NOT on the run's own DB the @@ -1004,19 +1004,19 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // the #new DB is FK-free on these columns. // =========================================================================================== - // A ksuid run on #new blocking on a cuid token resident on + // A run-ops run on #new blocking on a cuid token resident on // #legacy writes the block edge (TaskRunWaitpoint + WaitpointRunConnection) on #new, NOT requiring - // the waitpoint row to be local. The #legacy DB holds NO edge for the ksuid run. + // the waitpoint row to be local. The #legacy DB holds NO edge for the run-ops run. heteroRunOpsPostgresTest( "a NEW run blocking on a LEGACY-resident token writes the edge on NEW (no local waitpoint required)", async ({ prisma14, prisma17 }) => { const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3b_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap3b_leg"); - const runId = `run_${NEW_ID_26}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // run-ops run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid standalone token → resides on #legacy - // The ksuid run lives on #new. + // The run-ops run lives on #new. await prisma17.taskRun.create({ data: { id: runId, @@ -1069,7 +1069,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche where: { taskRunId: runId }, }); expect(connectionsOnNew).toBe(1); - // The #legacy DB holds NO edge for the ksuid run (the safety invariant: no cross-ref on LEGACY). + // The #legacy DB holds NO edge for the run-ops run (the safety invariant: no cross-ref on LEGACY). const edgesOnLegacy = await prisma14.taskRunWaitpoint.count({ where: { taskRunId: runId } }); expect(edgesOnLegacy).toBe(0); @@ -1084,7 +1084,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // Single-store cross-check: the #new store ALSO writes the edge directly (proving the fix is in // the store writer, not only the router routing). - const runId2 = `run_${"m".repeat(24) + "01"}`; // a second ksuid run on #new + const runId2 = `run_${"m".repeat(24) + "01"}`; // a second run-ops run on #new await prisma17.taskRun.create({ data: { id: runId2, @@ -1117,7 +1117,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche } ); - // Co-resident control: a ksuid run blocking on a CO-RESIDENT ksuid token still writes the + // Co-resident control: a run-ops run blocking on a CO-RESIDENT run-ops token still writes the // edge on #new (proving the fix didn't break the co-resident case the old join handled). heteroRunOpsPostgresTest( "control: a NEW run blocking on a CO-RESIDENT NEW token writes the edge on NEW", @@ -1125,7 +1125,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap3bco_new"); const runId = `run_${NEW_ID_26}`; - const waitpointId = `waitpoint_${NEW_ID_26}`; // ksuid token → co-resident on #new + const waitpointId = `waitpoint_${NEW_ID_26}`; // run-ops token → co-resident on #new await prisma17.taskRun.create({ data: { @@ -1301,14 +1301,14 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche // waitpoint's blocking edge, run connection and completing snapshot all CO-LOCATE WITH THE RUN // (blockRunWithWaitpointEdges routes by runId; the CompletedWaitpoint + WaitpointRunConnection // join rows are written on the run's DB). A cuid token blocking - // a ksuid run therefore has every group-A TARGET on the OTHER DB → the single-client hydrator finds + // a run-ops run therefore has every group-A TARGET on the OTHER DB → the single-client hydrator finds // nothing → engine.getWaitpoint (include blockingTaskRuns→taskRun) silently returns an // empty `blockingTaskRuns`. Fix: the router (RoutingRunStore.findWaitpoint/findManyWaitpoints) strips // these relation keys from the per-leg query and re-resolves the targets across BOTH DBs, mirroring // findManyTaskRunWaitpoints' edge fan-out. // =========================================================================================== - // A cuid token on #legacy blocking a ksuid run on #new. The block edge + run connection live + // A cuid token on #legacy blocking a run-ops run on #new. The block edge + run connection live // on #new (the run's DB). getWaitpoint's include{ blockingTaskRuns: { select: { taskRun } } } must // surface the cross-DB blocked run. Single-store guard proves the #legacy hydrator alone misses it. heteroRunOpsPostgresTest( @@ -1317,7 +1317,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche const { router, legacyStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap13bt_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap13bt_leg"); - const runId = `run_${NEW_ID_26}`; // ksuid run → #new + const runId = `run_${NEW_ID_26}`; // run-ops run → #new const waitpointId = `waitpoint_${CUID_25}`; // cuid token → #legacy await router.createRun( @@ -1380,7 +1380,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche ); // Sibling: connectedRuns. The WaitpointRunConnection join is co-resident with the run (#new), - // so a cuid token's connectedRuns must be re-resolved across BOTH DBs to surface the ksuid run. + // so a cuid token's connectedRuns must be re-resolved across BOTH DBs to surface the run-ops run. heteroRunOpsPostgresTest( "findWaitpoint include connectedRuns surfaces a cross-DB connected run via the router", async ({ prisma14, prisma17 }) => { @@ -1471,7 +1471,7 @@ describe("run-ops split — store-level behavior against the REAL dedicated sche environmentId: legEnv.environment.id, }, }); - // The snapshot (on #new, co-resident with the ksuid run) records the cross-DB token as completed + // The snapshot (on #new, co-resident with the run-ops run) records the cross-DB token as completed // via the CompletedWaitpoint join. const snapshot = await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, diff --git a/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts b/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts index a058a939c0a..031d575a3e2 100644 --- a/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts @@ -2,7 +2,7 @@ // // Under the run-ops split, several engine operations that were atomic-by-`prisma.$transaction` in // single-DB make TWO distinct RunStore writes (e.g. startAttempt + createExecutionSnapshot, or -// promotePendingVersionRuns + createExecutionSnapshot). When the run is ksuid (#new), `RoutingRunStore` +// promotePendingVersionRuns + createExecutionSnapshot). When the run is run-ops id (#new), `RoutingRunStore` // routes each write to the NEW store but DROPS the caller's control-plane `tx` — so the two writes // execute as independent auto-commit statements on the NEW DB, OUTSIDE any shared transaction. A crash // between them leaves partial state (a run EXECUTING with no matching snapshot; promoted-but-no-snapshot). @@ -26,7 +26,7 @@ import type { CreateRunInput, RunStore, RunStoreSchemaVariant } from "./types.js type AnyClient = PrismaClient | RunOpsPrismaClient; -// ownerEngine classifies by the version char: no marker → cuid → LEGACY, v1 body → ksuid → NEW. +// ownerEngine classifies by the version char: no marker → cuid → LEGACY, v1 body → run-ops id → NEW. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / control-plane DB, full schema) const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / dedicated run-ops DB, subset schema) @@ -136,8 +136,8 @@ function makeSplitRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { }; } -// Seed a ksuid run on #new (its create nests the initial RUN_CREATED snapshot) and return its ids. -async function seedKsuidRun( +// Seed a run-ops run on #new (its create nests the initial RUN_CREATED snapshot) and return its ids. +async function seedRunOpsRun( router: RunStore, prisma17: RunOpsPrismaClient, suffix: string @@ -175,14 +175,14 @@ function snapshotInput( describe("cross-DB write atomicity (startAttempt + createExecutionSnapshot)", () => { // --------------------------------------------------------------------------------------------- // RED demonstration: the BROKEN behaviour. Two separate routed writes (as the engine made them - // before the fix) on a ksuid run leave PARTIAL state on a mid-pair failure — the run is EXECUTING + // before the fix) on a run-ops run leave PARTIAL state on a mid-pair failure — the run is EXECUTING // but no EXECUTING snapshot exists. This is the regression vs single-DB. // --------------------------------------------------------------------------------------------- heteroRunOpsPostgresTest( "BROKEN baseline: two un-wrapped routed writes persist partial state on a mid-pair failure", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); - const { runId, env } = await seedKsuidRun(router, prisma17, "broken_atomic"); + const { runId, env } = await seedRunOpsRun(router, prisma17, "broken_atomic"); // Simulate the OLD engine pattern: startAttempt then a failure BEFORE createExecutionSnapshot, // each as an independent routed (auto-commit) write — no shared transaction. @@ -214,10 +214,10 @@ describe("cross-DB write atomicity (startAttempt + createExecutionSnapshot)", () // BETWEEN the two writes rolls the FIRST write back — no partial state. // --------------------------------------------------------------------------------------------- heteroRunOpsPostgresTest( - "runInTransaction rolls back startAttempt when a failure is injected before the snapshot write (ksuid → #new)", + "runInTransaction rolls back startAttempt when a failure is injected before the snapshot write (run-ops id → #new)", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); - const { runId, env } = await seedKsuidRun(router, prisma17, "rollback_new"); + const { runId, env } = await seedRunOpsRun(router, prisma17, "rollback_new"); await expect( router.runInTransaction(runId, async (store, tx) => { @@ -246,10 +246,10 @@ describe("cross-DB write atomicity (startAttempt + createExecutionSnapshot)", () ); heteroRunOpsPostgresTest( - "runInTransaction commits BOTH writes atomically on success (ksuid → #new)", + "runInTransaction commits BOTH writes atomically on success (run-ops id → #new)", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); - const { runId, env } = await seedKsuidRun(router, prisma17, "commit_new"); + const { runId, env } = await seedRunOpsRun(router, prisma17, "commit_new"); const result = await router.runInTransaction(runId, async (store, tx) => { const run = await store.startAttempt( diff --git a/internal-packages/run-store/src/batchCompletionResidency.test.ts b/internal-packages/run-store/src/batchCompletionResidency.test.ts index 9f2ccd12fa5..e8b42893ada 100644 --- a/internal-packages/run-store/src/batchCompletionResidency.test.ts +++ b/internal-packages/run-store/src/batchCompletionResidency.test.ts @@ -1,7 +1,7 @@ // REGRESSION suite for the run-ops split "control-plane tx/client forwarded into a NEW-resident // store" bug class on the BatchTaskRun write/probe path. When the router resolves the owning store // to #new but forwards the caller's control-plane handle, #new issues its statement against the -// CONTROL-PLANE DB where the ksuid row does not exist → "No record was found" (update), wrong-DB row +// CONTROL-PLANE DB where the run-ops id row does not exist → "No record was found" (update), wrong-DB row // (create), or wrong count. Covers updateBatchTaskRun (commit 62ae880af), createBatchTaskRun and // countBatchTaskRunItems (this sweep). `heteroRunOpsPostgresTest` is the REAL two-DB split topology // (prisma17 = dedicated #new, prisma14 = legacy #legacy); NEVER mocked. @@ -118,16 +118,16 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont // updateBatchTaskRun (commit 62ae880af) — the batch-completion residency regression. // =========================================================================================== - // The live `batchSystem.#tryCompleteBatch` shape: a ksuid batch on #new is updated to COMPLETED + // The live `batchSystem.#tryCompleteBatch` shape: a run-ops batch on #new is updated to COMPLETED // while the control-plane client is passed as `tx`. RED on the pre-62ae880af code (the router // forwarded tx → #new ran the UPDATE on the control-plane DB → "No record was found for an // update"); GREEN now (tx dropped for NEW → the row updates on #new's own DB). heteroRunOpsPostgresTest( - "updateBatchTaskRun marks a ksuid batch on #new COMPLETED even when the control-plane client is passed as tx", + "updateBatchTaskRun marks a run-ops batch on #new COMPLETED even when the control-plane client is passed as tx", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "updbatch_new"); - const batchId = `batch_${NEW_ID_26}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id → #new await prisma17.batchTaskRun.create({ data: { @@ -185,15 +185,15 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont // createBatchTaskRun (this sweep) — same anti-pattern on the create path. // =========================================================================================== - // A ksuid batch routed to #new with a forwarded control-plane tx must still be created on #new's + // A run-ops batch routed to #new with a forwarded control-plane tx must still be created on #new's // OWN DB, not the control-plane DB (which would strand the batch away from its co-resident child // runs/items). Forwarding tx unconditionally would land the row on prisma14. heteroRunOpsPostgresTest( - "createBatchTaskRun lands a ksuid batch on #new even when the control-plane client is passed as tx", + "createBatchTaskRun lands a run-ops batch on #new even when the control-plane client is passed as tx", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "crbatch_new"); - const batchId = `batch_${NEW_ID_26}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id → #new const created = await router.createBatchTaskRun( { @@ -206,7 +206,7 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont ); expect(created.id).toBe(batchId); - // Resident on #new (its own DB), absent from #legacy — co-located with its ksuid child runs. + // Resident on #new (its own DB), absent from #legacy — co-located with its run-ops child runs. const onNew = await prisma17.batchTaskRun.findUnique({ where: { id: batchId } }); expect(onNew).not.toBeNull(); const onLegacy = await prisma14.batchTaskRun.findUnique({ where: { id: batchId } }); @@ -243,14 +243,14 @@ describe("run-ops split — BatchTaskRun writes/probes must NOT forward the cont // countBatchTaskRunItems (this sweep) — same anti-pattern on a routed probe read. // =========================================================================================== - // A ksuid batch's items live on #new; counting them with the control-plane client forwarded would + // A run-ops batch's items live on #new; counting them with the control-plane client forwarded would // count on the wrong DB (→ 0). The routed store must read its OWN DB and return the real count. heteroRunOpsPostgresTest( - "countBatchTaskRunItems counts a ksuid batch's items on #new even when the control-plane client is passed", + "countBatchTaskRunItems counts a run-ops batch's items on #new even when the control-plane client is passed", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cntitems_new"); - const batchId = `batch_${NEW_ID_26}`; // ksuid → #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id → #new await prisma17.batchTaskRun.create({ data: { diff --git a/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts b/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts index 541a9e09401..373984bc343 100644 --- a/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts +++ b/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts @@ -3,7 +3,7 @@ // // During the flip window, two CONCURRENT same-(env, idempotencyKey, taskIdentifier) ROOT triggers can // land on instances with DIVERGENT cached mint-kinds: the stale instance mints a cuid run on #legacy, -// the flipped instance a ksuid run on #new. The dedup probe (probe-before-mint) only catches an +// the flipped instance a run-ops run on #new. The dedup probe (probe-before-mint) only catches an // ALREADY-COMMITTED run; two truly-simultaneous mints both miss, then both create. The per-DB unique // constraint on (runtimeEnvironmentId, idempotencyKey, taskIdentifier) is PER PHYSICAL DB, so it // cannot reject the second insert that lands on the OTHER DB. This test proves both creates SUCCEED @@ -22,9 +22,9 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id length (no internal underscore): 25 -> cuid -> LEGACY, -// 27 -> ksuid -> NEW. +// 27 -> run-ops id -> NEW. const cuidLegacy = (seed: string) => (seed + "c".repeat(25)).slice(0, 25); -const ksuidNew = (seed: string) => +const runOpsNew = (seed: string) => (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; async function seedEnvironment( @@ -138,7 +138,7 @@ describe("RoutingRunStore — mint-on-flip bounded concurrent-window cross-DB du const taskIdentifier = "my-task"; const staleCuidRunId = cuidLegacy("rfl"); // stale instance mints cuid -> #legacy - const flippedKsuidRunId = ksuidNew("rfn"); // flipped instance mints ksuid -> #new + const flippedRunOpsRunId = runOpsNew("rfn"); // flipped instance mints run-ops id -> #new // Both concurrent mints commit. The second does NOT throw a unique violation: the constraint is // PER-DB and these land on different physical DBs. @@ -155,7 +155,7 @@ describe("RoutingRunStore — mint-on-flip bounded concurrent-window cross-DB du ); await router.createRun( buildCreateRunInput({ - runId: flippedKsuidRunId, + runId: flippedRunOpsRunId, friendlyId: "run_flip_new", organizationId: seed.organization.id, projectId: seed.project.id, @@ -167,7 +167,9 @@ describe("RoutingRunStore — mint-on-flip bounded concurrent-window cross-DB du // The duplicate is REAL: a row for the same key physically exists on BOTH DBs. expect(await prisma14.taskRun.findFirst({ where: { id: staleCuidRunId } })).not.toBeNull(); - expect(await prisma17.taskRun.findFirst({ where: { id: flippedKsuidRunId } })).not.toBeNull(); + expect( + await prisma17.taskRun.findFirst({ where: { id: flippedRunOpsRunId } }) + ).not.toBeNull(); // The duplicate is BOUNDED: subsequent reads via the id-less probe collapse to exactly ONE run, // deterministically the NEW one (NEW-first fan-out) — the same tie-break the cross-DB dedup @@ -178,7 +180,7 @@ describe("RoutingRunStore — mint-on-flip bounded concurrent-window cross-DB du taskIdentifier, })) as Record | null; expect(found).not.toBeNull(); - expect(found!.id).toBe(flippedKsuidRunId); + expect(found!.id).toBe(flippedRunOpsRunId); } ); }); diff --git a/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts b/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts index a943d407337..eff2a71cd97 100644 --- a/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts +++ b/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts @@ -5,7 +5,7 @@ import type { RunStore } from "./types.js"; // forWaitpointCompletion is async: it picks a preferred store from the id-shape + pins, then // PROBES findWaitpoint to resolve where the token ACTUALLY lives (drain can relocate a cuid -// waitpoint onto NEW, or a ksuid token can be pinned LEGACY), falling back to the other store. +// waitpoint onto NEW, or a run-ops token can be pinned LEGACY), falling back to the other store. // So the slots here are fakes whose only behaviour is "do I hold this waitpoint id?". function fakeStore(slot: string, heldIds: Set): RunStore { return { @@ -17,7 +17,7 @@ function fakeStore(slot: string, heldIds: Set): RunStore { } as unknown as RunStore; } -const KSUID_ID = "waitpoint_" + "a".repeat(24) + "01"; +const RUN_OPS_ID = "waitpoint_" + "a".repeat(24) + "01"; const CUID_ID = "waitpoint_" + "a".repeat(25); const UNCLASSIFIABLE_ID = "waitpoint_" + "a".repeat(26); @@ -28,7 +28,7 @@ function buildRouter(opts?: { newHolds?: string[]; legacyHolds?: string[] }): { newStore: RunStore; legacyStore: RunStore; } { - const all = [KSUID_ID, CUID_ID, UNCLASSIFIABLE_ID]; + const all = [RUN_OPS_ID, CUID_ID, UNCLASSIFIABLE_ID]; const newStore = fakeStore("new", new Set(opts?.newHolds ?? all)); const legacyStore = fakeStore("legacy", new Set(opts?.legacyHolds ?? all)); return { @@ -39,9 +39,9 @@ function buildRouter(opts?: { newHolds?: string[]; legacyHolds?: string[] }): { } describe("RoutingRunStore.forWaitpointCompletion", () => { - it("resolves a ksuid waitpointId with no pins to the NEW slot", async () => { + it("resolves a run-ops waitpointId with no pins to the NEW slot", async () => { const { router, newStore } = buildRouter(); - expect(await router.forWaitpointCompletion(KSUID_ID, { routeKind: "MANUAL" })).toBe(newStore); + expect(await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "MANUAL" })).toBe(newStore); }); it("resolves a cuid waitpointId with no pins to the LEGACY slot", async () => { @@ -49,30 +49,30 @@ describe("RoutingRunStore.forWaitpointCompletion", () => { expect(await router.forWaitpointCompletion(CUID_ID, { routeKind: "MANUAL" })).toBe(legacyStore); }); - it("pins to LEGACY when isCrossTreeIdempotency is true, even for a ksuid id", async () => { + it("pins to LEGACY when isCrossTreeIdempotency is true, even for a run-ops id", async () => { const { router, legacyStore } = buildRouter(); expect( - await router.forWaitpointCompletion(KSUID_ID, { + await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "IDEMPOTENCY_REUSE", isCrossTreeIdempotency: true, }) ).toBe(legacyStore); }); - it("pins to LEGACY when treeOwnerResidency is LEGACY, even for a ksuid id", async () => { + it("pins to LEGACY when treeOwnerResidency is LEGACY, even for a run-ops id", async () => { const { router, legacyStore } = buildRouter(); expect( - await router.forWaitpointCompletion(KSUID_ID, { + await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "MANUAL", treeOwnerResidency: "LEGACY", }) ).toBe(legacyStore); }); - it("pins to LEGACY when hasLegacyParent is true, even for a ksuid id", async () => { + it("pins to LEGACY when hasLegacyParent is true, even for a run-ops id", async () => { const { router, legacyStore } = buildRouter(); expect( - await router.forWaitpointCompletion(KSUID_ID, { + await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "RUN", hasLegacyParent: true, }) @@ -80,10 +80,10 @@ describe("RoutingRunStore.forWaitpointCompletion", () => { }); it("falls back to the OTHER store when the preferred store does not hold the token", async () => { - // ksuid id prefers NEW, but the token actually lives on LEGACY (drain/relocation): the + // run-ops id prefers NEW, but the token actually lives on LEGACY (drain/relocation): the // probe must fall through to LEGACY rather than route by id-shape alone and miss it. - const { router, legacyStore } = buildRouter({ newHolds: [], legacyHolds: [KSUID_ID] }); - expect(await router.forWaitpointCompletion(KSUID_ID, { routeKind: "MANUAL" })).toBe( + const { router, legacyStore } = buildRouter({ newHolds: [], legacyHolds: [RUN_OPS_ID] }); + expect(await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "MANUAL" })).toBe( legacyStore ); }); diff --git a/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts b/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts index 6cd1838397c..3546ea197c9 100644 --- a/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts +++ b/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts @@ -4,7 +4,7 @@ // `runStore.findRun({ runtimeEnvironmentId, idempotencyKey, taskIdentifier }, // { include: { associatedWaitpoint: true } }, dedupClient)` // (apps/webapp/app/runEngine/concerns/idempotencyKeys.server.ts). The existing run may live -// on EITHER physical DB (a cuid run on #legacy minted before the org flipped to ksuid; a ksuid run on +// on EITHER physical DB (a cuid run on #legacy minted before the org flipped to run-ops id; a run-ops run on // #new after). The PG unique key is PER-DB and cannot enforce cross-DB uniqueness, so dedup must be // correct at the routing layer. RoutingRunStore.findRun drops the caller // dedupClient and, for an id-less where, fans out NEW→LEGACY (#findRunUnrouted). @@ -27,12 +27,12 @@ import type { type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH after stripping a single leading `_`: -// 25 chars → cuid → LEGACY, a v1 body (version "1" at index 25) → ksuid → NEW. So a classifiable id +// 25 chars → cuid → LEGACY, a v1 body (version "1" at index 25) → run-ops id → NEW. So a classifiable id // must carry NO internal underscore. These mint a distinct id of the right length from a short seed. function cuidLegacy(seed: string): string { return (seed + "c".repeat(25)).slice(0, 25); // 25 chars, no underscore → LEGACY } -function ksuidNew(seed: string): string { +function runOpsNew(seed: string): string { return (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; } @@ -240,16 +240,16 @@ describe("RoutingRunStore — cross-DB idempotency dedup probe", () => { } ); - // the matching run + its associated waitpoint live on #new (ksuid, dedicated subset). The + // the matching run + its associated waitpoint live on #new (run-ops id, dedicated subset). The // probe hits the NEW leg first; the SCALAR-ONLY store must strip the `associatedWaitpoint` relation // and re-hydrate it from `Waitpoint.completedByTaskRunId`. heteroRunOpsPostgresTest( - "a ksuid run on #new is found by the id-less probe with associatedWaitpoint hydrated from scalar", + "a run-ops run on #new is found by the id-less probe with associatedWaitpoint hydrated from scalar", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cg2_b"); - const runId = ksuidNew("rbn"); // v1 body → NEW home - const waitpointId = ksuidNew("wbn"); + const runId = runOpsNew("rbn"); // v1 body → NEW home + const waitpointId = runOpsNew("wbn"); const idempotencyKey = "cg2-key-b"; const taskIdentifier = "my-task"; @@ -271,7 +271,7 @@ describe("RoutingRunStore — cross-DB idempotency dedup probe", () => { }) ); - // It must NOT have landed on #legacy (the ksuid id routes to NEW). + // It must NOT have landed on #legacy (the run-ops id routes to NEW). expect(await prisma14.taskRun.findFirst({ where: { id: runId } })).toBeNull(); expect(await prisma17.taskRun.findFirst({ where: { id: runId } })).not.toBeNull(); @@ -293,7 +293,7 @@ describe("RoutingRunStore — cross-DB idempotency dedup probe", () => { // duplicate-guard contract: a run with the SAME (env, idempotencyKey, taskIdentifier) // exists on BOTH DBs. The per-DB unique constraint allows one row each (it cannot enforce cross-DB - // uniqueness); the probe MUST still resolve to exactly ONE run, deterministically the NEW (ksuid) + // uniqueness); the probe MUST still resolve to exactly ONE run, deterministically the NEW (run-ops id) // one per #findRunUnrouted (NEW-first). The duplicate itself is prevented upstream by // probe-before-mint plus the per-DB unique constraint; this locks the read tie-break contract. heteroRunOpsPostgresTest( @@ -307,9 +307,9 @@ describe("RoutingRunStore — cross-DB idempotency dedup probe", () => { const taskIdentifier = "my-task"; const legacyRunId = cuidLegacy("rcl"); // cuid → LEGACY - const newRunId = ksuidNew("rcn"); // ksuid → NEW + const newRunId = runOpsNew("rcn"); // run-ops id → NEW const legacyWaitpointId = cuidLegacy("wcl"); - const newWaitpointId = ksuidNew("wcn"); + const newWaitpointId = runOpsNew("wcn"); await router.createRun( buildCreateRunInput({ @@ -392,7 +392,7 @@ describe("RoutingRunStore — cross-DB idempotency dedup probe", () => { async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cg2_sa_n"); - const runId = ksuidNew("rsan"); // ksuid → NEW + const runId = runOpsNew("rsan"); // run-ops id → NEW const idempotencyKey = "cg2-key-sa-n"; await router.createRun( diff --git a/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts b/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts index 2c13ce3fd2b..ca2027d5723 100644 --- a/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts +++ b/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts @@ -1,10 +1,10 @@ // MIXED-RESIDENCY MATRIX — systematic LOCK that every RoutingRunStore fan-out / partition / merge / -// dedup method behaves correctly when cuid (#legacy) AND ksuid (#new) data COEXIST in the SAME call, +// dedup method behaves correctly when cuid (#legacy) AND run-ops id (#new) data COEXIST in the SAME call, // against the REAL two-physical-DB split (heteroRunOpsPostgresTest: prisma14 = full/legacy on PG14, // prisma17 = RunOpsPrismaClient / dedicated subset on PG17). NEVER mocked. // // Existing tests exercise these methods one residency at a time or for a single specific bug. This -// file is the cross-residency matrix: each case seeds BOTH a cuid row on #legacy AND a ksuid row on +// file is the cross-residency matrix: each case seeds BOTH a cuid row on #legacy AND a run-ops id row on // #new in one environment, then drives the wired router and asserts the merge/partition is correct. // The matrix MUST go RED if a fan-out leg is dropped or a NEW-wins dedup regresses (see the reverted // mutation probes recorded in the task report). @@ -20,12 +20,12 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH after stripping a leading `_` -// (runOpsResidency.ts): 25 chars (no internal underscore) → cuid → LEGACY, a v1 body (version "1" at index 25) → ksuid → NEW. +// (runOpsResidency.ts): 25 chars (no internal underscore) → cuid → LEGACY, a v1 body (version "1" at index 25) → run-ops id → NEW. // These mint a distinct classifiable id of the right length from a short seed. function cuidLegacy(seed: string): string { return (seed + "c".repeat(25)).slice(0, 25); // 25 chars → LEGACY (#legacy / prisma14) } -function ksuidNew(seed: string): string { +function runOpsNew(seed: string): string { return (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; } @@ -194,11 +194,11 @@ async function seedSharedEnv(prisma14: PrismaClient, suffix: string) { }; } -describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new coexisting)", () => { +describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + run-ops id #new coexisting)", () => { // ── Case 1: findRuns by a MIXED bounded id-set (#findRunsByIdSet, runOpsStore.ts:294) ── - // A list-hydrate id set spans cuid (legacy) + ksuid (new) ids plus a ksuid id absent from legacy. + // A list-hydrate id set spans cuid (legacy) + run-ops id (new) ids plus a run-ops id absent from legacy. // Both resident runs returned; take/skip applied GLOBALLY post-merge; orderBy honored; the absent - // ksuid short-circuits (never probed on LEGACY, :309). + // run-ops id short-circuits (never probed on LEGACY, :309). heteroRunOpsPostgresTest( "case 1: findRuns by a mixed id-set returns both DBs' runs, ordered, take/skip global", async ({ prisma14, prisma17 }) => { @@ -206,8 +206,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m1"); const legacyId = cuidLegacy("m1l"); // cuid → #legacy - const newId = ksuidNew("m1n"); // ksuid → #new - const ghostKsuid = ksuidNew("m1g"); // ksuid, NEVER created → tests the LEGACY short-circuit + const newId = runOpsNew("m1n"); // run-ops id → #new + const ghostRunOpsId = runOpsNew("m1g"); // run-ops id, NEVER created → tests the LEGACY short-circuit await router.createRun( buildCreateRunInput({ @@ -234,7 +234,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new // Full merge, ordered by createdAt asc → newId (Jan 1) before legacyId (Jan 2). const all = await router.findRuns({ - where: { id: { in: [legacyId, newId, ghostKsuid] } }, + where: { id: { in: [legacyId, newId, ghostRunOpsId] } }, select: { id: true, createdAt: true }, orderBy: { createdAt: "asc" }, }); @@ -243,7 +243,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new // take=1 after the merge → only the first (newId). Proves take is applied GLOBALLY, not per-leg // (a per-leg take=1 would return one row from EACH DB → both ids). const firstOnly = await router.findRuns({ - where: { id: { in: [legacyId, newId, ghostKsuid] } }, + where: { id: { in: [legacyId, newId, ghostRunOpsId] } }, select: { id: true }, orderBy: { createdAt: "asc" }, take: 1, @@ -252,7 +252,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new // skip=1 take=1 → the second (legacyId). const second = await router.findRuns({ - where: { id: { in: [legacyId, newId, ghostKsuid] } }, + where: { id: { in: [legacyId, newId, ghostRunOpsId] } }, select: { id: true }, orderBy: { createdAt: "asc" }, skip: 1, @@ -319,7 +319,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m2"); const legacyId = cuidLegacy("m2l"); - const newId = ksuidNew("m2n"); + const newId = runOpsNew("m2n"); await router.createRun( buildCreateRunInput({ runId: legacyId, @@ -342,7 +342,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new ); await router.createRun( buildCreateRunInput({ - runId: ksuidNew("m2np"), + runId: runOpsNew("m2np"), friendlyId: "run_m2_new_pending", status: "PENDING", ...env, @@ -359,7 +359,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new ); // ── Case 3: expireRunsBatch with a MIXED id list (runOpsStore.ts:474) ── - // Partitions ksuid→NEW / cuid→LEGACY; each leg called only when non-empty; counts summed; each row + // Partitions run-ops id→NEW / cuid→LEGACY; each leg called only when non-empty; counts summed; each row // updated on its OWN DB only. heteroRunOpsPostgresTest( "case 3: expireRunsBatch partitions a mixed id list per-DB and sums the count", @@ -368,7 +368,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m3"); const legacyId = cuidLegacy("m3l"); - const newId = ksuidNew("m3n"); + const newId = runOpsNew("m3n"); await router.createRun( buildCreateRunInput({ runId: legacyId, friendlyId: "run_m3_legacy", ...env }) ); @@ -400,7 +400,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m4"); const legacyId = cuidLegacy("m4l"); - const newId = ksuidNew("m4n"); + const newId = runOpsNew("m4n"); await router.createRun( buildCreateRunInput({ runId: legacyId, @@ -441,8 +441,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m5"); const legacyWp = cuidLegacy("m5l"); // PENDING on #legacy - const newWp = ksuidNew("m5n"); // PENDING on #new - const completedWp = ksuidNew("m5c"); // COMPLETED on #new → must NOT be counted + const newWp = runOpsNew("m5n"); // PENDING on #new + const completedWp = runOpsNew("m5c"); // COMPLETED on #new → must NOT be counted await seedPendingWaitpoint(prisma14, { id: legacyWp, friendlyId: "wp_m5_legacy", @@ -477,7 +477,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m6"); const legacyWp = cuidLegacy("m6l"); - const newWp = ksuidNew("m6n"); + const newWp = runOpsNew("m6n"); await seedPendingWaitpoint(prisma14, { id: legacyWp, friendlyId: "wp_m6_legacy", @@ -499,7 +499,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new // ── Case 8: findExecutionSnapshot / findManyExecutionSnapshots OPEN (no runId) where ── // A by-snapshot-id-only lookup (snapshot ids are non-classifiable cuids) must fan out NEW→LEGACY // (findExecutionSnapshot, :675) / merge both (findManyExecutionSnapshots, :688). Seed a snapshot on - // EACH DB (one ksuid run on #new, one cuid run on #legacy) and read with a no-runId where. + // EACH DB (one run-ops run on #new, one cuid run on #legacy) and read with a no-runId where. heteroRunOpsPostgresTest( "case 8: findExecutionSnapshot/findManyExecutionSnapshots with an open where reach both DBs", async ({ prisma14, prisma17 }) => { @@ -507,7 +507,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m8"); const legacyRun = cuidLegacy("m8l"); - const newRun = ksuidNew("m8n"); + const newRun = runOpsNew("m8n"); await router.createRun( buildCreateRunInput({ runId: legacyRun, friendlyId: "run_m8_legacy", ...env }) ); @@ -548,7 +548,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new ); // ── Case 9a: findRun with an UNCLASSIFIABLE where (spanId) on a #legacy run (#findRunUnrouted, :213) ── - // A ksuid run on #new and a cuid run on #legacy each carry a distinct spanId. A spanId where can't + // A run-ops run on #new and a cuid run on #legacy each carry a distinct spanId. A spanId where can't // be id-classified → fan out NEW-first then LEGACY. The legacy-resident run must be found. heteroRunOpsPostgresTest( "case 9a: findRun by spanId fans out and finds a #legacy run (NEW miss → LEGACY hit)", @@ -557,7 +557,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m9a"); const legacyRun = cuidLegacy("m9al"); - const newRun = ksuidNew("m9an"); + const newRun = runOpsNew("m9an"); await router.createRun( buildCreateRunInput({ runId: legacyRun, @@ -599,7 +599,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m9b"); const legacyRun = cuidLegacy("m9bl"); - const newRun = ksuidNew("m9bn"); + const newRun = runOpsNew("m9bn"); await router.createRun( buildCreateRunInput({ runId: legacyRun, @@ -633,7 +633,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new // ── Case 7: findManyTaskRunWaitpoints with edges whose relations STRADDLE DBs (runOpsStore.ts:876) ── // An edge co-locates with its RUN, but its `waitpoint`/`taskRun` relations can live on the OTHER DB - // (a cuid token blocking a ksuid run, and vice versa). The per-leg scalar query is stripped of the + // (a cuid token blocking a run-ops run, and vice versa). The per-leg scalar query is stripped of the // relation keys; the router re-hydrates `waitpoint`/`taskRun` across BOTH DBs. Exercises BOTH // straddle directions in one read by querying both edges via { taskRunId: { in } }. heteroRunOpsPostgresTest( @@ -642,8 +642,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedSharedEnv(prisma14, "m7"); - // Direction A: ksuid run on #new, blocked on a cuid token that lives ONLY on #legacy. Edge on #new. - const newRun = ksuidNew("m7nr"); + // Direction A: run-ops run on #new, blocked on a cuid token that lives ONLY on #legacy. Edge on #new. + const newRun = runOpsNew("m7nr"); const legacyToken = cuidLegacy("m7lt"); await router.createRun( buildCreateRunInput({ runId: newRun, friendlyId: "run_m7_new", ...env }) @@ -660,12 +660,12 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new `INSERT INTO "TaskRunWaitpoint" ("id","taskRunId","waitpointId","projectId","createdAt","updatedAt") VALUES (gen_random_uuid(),'${newRun}','${legacyToken}','${env.projectId}',NOW(),NOW())` ); - // Direction B: cuid run on #legacy, blocked on a ksuid token mirrored onto BOTH DBs (drain + // Direction B: cuid run on #legacy, blocked on a run-ops token mirrored onto BOTH DBs (drain // window). The #legacy copy is a STALE placeholder (PENDING) that satisfies the legacy edge FK; // the AUTHORITATIVE #new copy is COMPLETED. Edge on #legacy. Hydration re-resolves cross-DB and // NEW-wins the dedup → the edge's waitpoint must read the #new (COMPLETED) copy, not the local mirror. const legacyRun = cuidLegacy("m7lr"); - const newToken = ksuidNew("m7nt"); + const newToken = runOpsNew("m7nt"); await router.createRun( buildCreateRunInput({ runId: legacyRun, friendlyId: "run_m7_legacy", ...env }) ); @@ -733,8 +733,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedSharedEnv(prisma14, "m7b"); - const newRun = ksuidNew("m7br"); - const ghostToken = ksuidNew("m7bg"); // never created on either DB + const newRun = runOpsNew("m7br"); + const ghostToken = runOpsNew("m7bg"); // never created on either DB await router.createRun( buildCreateRunInput({ runId: newRun, friendlyId: "run_m7b_new", ...env }) ); @@ -752,8 +752,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new ); // ── Case 10: findBatchTaskRunById / findBatchTaskRunByFriendlyId NEW-then-LEGACY probe (:1124,:1137) ── - // A batch resident on #legacy AND a ksuid-id batch landed on #new (the control-plane window mints - // cuid ids, but a ksuid batch resides on #new) are BOTH found via the probe, regardless of id-shape. + // A batch resident on #legacy AND a run-ops-id batch landed on #new (the control-plane window mints + // cuid ids, but a run-ops batch resides on #new) are BOTH found via the probe, regardless of id-shape. heteroRunOpsPostgresTest( "case 10: findBatchTaskRunById/byFriendlyId probe NEW then LEGACY and find batches on either DB", async ({ prisma14, prisma17 }) => { @@ -761,7 +761,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m10"); const legacyBatch = cuidLegacy("m10l"); // cuid → #legacy - const newBatch = ksuidNew("m10n"); // ksuid → #new + const newBatch = runOpsNew("m10n"); // run-ops id → #new await prisma14.batchTaskRun.create({ data: { id: legacyBatch, @@ -805,7 +805,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m11a"); const legacyWp = cuidLegacy("m11al"); - const newWp = ksuidNew("m11an"); + const newWp = runOpsNew("m11an"); await seedPendingWaitpoint(prisma14, { id: legacyWp, friendlyId: "wp_m11a_legacy", @@ -846,7 +846,7 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new // ONE logical run id whose edges happen to exist on BOTH DBs (the straddle the fan-out guards). // The edge is FK-free on #new (unnest path) and FK-bound on #legacy, so seed a co-resident // waitpoint + run on #legacy for its edge, and write the #new edge directly. - const runId = ksuidNew("m11br"); + const runId = runOpsNew("m11br"); const legacyToken = cuidLegacy("m11bt"); await router.createRun(buildCreateRunInput({ runId, friendlyId: "run_m11b", ...env })); // #legacy needs the run + token present for the FK-bound edge insert. @@ -881,8 +881,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + ksuid #new await prisma14.$executeRawUnsafe( `INSERT INTO "TaskRunWaitpoint" ("id","taskRunId","waitpointId","projectId","createdAt","updatedAt") VALUES (gen_random_uuid(),'${runId}','${legacyToken}','${env.projectId}',NOW(),NOW())` ); - // #new edge (FK-free) pointing at a ksuid token absent locally — drain straddle. - const newToken = ksuidNew("m11bn"); + // #new edge (FK-free) pointing at a run-ops token absent locally — drain straddle. + const newToken = runOpsNew("m11bn"); await prisma17.$executeRawUnsafe( `INSERT INTO "TaskRunWaitpoint" ("id","taskRunId","waitpointId","projectId","createdAt","updatedAt") VALUES (gen_random_uuid(),'${runId}','${newToken}','${env.projectId}',NOW(),NOW())` ); diff --git a/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts b/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts index 4fdcfe75042..a08408455ea 100644 --- a/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts +++ b/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts @@ -26,7 +26,7 @@ import { RoutingRunStore } from "./runOpsStore.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; -// ownerEngine classifies by internal-id LENGTH: 25 chars → cuid → LEGACY, 27 → ksuid → NEW. +// ownerEngine classifies by internal-id LENGTH: 25 chars → cuid → LEGACY, 27 → run-ops id → NEW. const CUID_25 = "c".repeat(25); // → LEGACY (#legacy / prisma14, full schema) const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW (#new / prisma17, dedicated subset schema) @@ -203,12 +203,12 @@ describe("run-ops split — read-after-write reads the OWNING store's WRITER, no } ); - // (b) NEW-resident (ksuid) run: born on the NEW DB (5434). The NEW replica lags. Passing the NEW + // (b) NEW-resident (run-ops id) run: born on the NEW DB (5434). The NEW replica lags. Passing the NEW // WRITER as the read-your-writes client must resolve the run via the NEW writer, NOT its replica — // and (proving the constraint that motivated the original client-drop) the control-plane writer is // never leaked into the NEW query: each store reads its OWN writer. heteroRunOpsPostgresTest( - "NEW ksuid: read-after-write via the NEW WRITER finds the fresh run despite NEW replica lag", + "NEW run-ops id: read-after-write via the NEW WRITER finds the fresh run despite NEW replica lag", async ({ prisma14, prisma17 }) => { const newReplica = laggingReplica(prisma17); const newStore = new PostgresRunStore({ @@ -224,7 +224,7 @@ describe("run-ops split — read-after-write reads the OWNING store's WRITER, no const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed = seedEnvironmentDedicated("raw_new"); - const runId = `run_${NEW_ID_26}`; // ksuid → NEW + const runId = `run_${NEW_ID_26}`; // run-ops id → NEW await prisma17.taskRun.create({ data: taskRunData({ id: runId, @@ -259,7 +259,7 @@ describe("run-ops split — read-after-write reads the OWNING store's WRITER, no expect(newReplica2.wasHit()).toBe(false); // Even passing the LEGACY (control-plane) WRITER as the read-your-writes signal resolves the - // ksuid run: the router routes by residency to the NEW store's OWN writer, never forwarding the + // run-ops run: the router routes by residency to the NEW store's OWN writer, never forwarding the // control-plane client into the NEW DB. (This is the exact live shape — sessions/trigger pass // the control-plane `prisma`, and the run may be NEW-resident under split-ON.) const newReplica3 = laggingReplica(prisma17); diff --git a/internal-packages/run-store/src/runOpsStore.snapshots.test.ts b/internal-packages/run-store/src/runOpsStore.snapshots.test.ts index 0e482e0e874..6ee9ca02116 100644 --- a/internal-packages/run-store/src/runOpsStore.snapshots.test.ts +++ b/internal-packages/run-store/src/runOpsStore.snapshots.test.ts @@ -61,7 +61,7 @@ async function seedEnvironment( } // ownerEngine classifies by the version char after stripping a single leading `_`: a v1 body -// → ksuid → NEW (#new / dedicated run-ops DB subset), 25 chars → cuid → LEGACY (#legacy / full schema). +// → run-ops id → NEW (#new / dedicated run-ops DB subset), 25 chars → cuid → LEGACY (#legacy / full schema). const NEW_ID_26 = "k".repeat(24) + "01"; // → NEW residency, exercises the dedicated store const CUID_25 = "c".repeat(25); // → LEGACY residency, exercises the full-schema store diff --git a/internal-packages/run-store/src/runOpsStore.test.ts b/internal-packages/run-store/src/runOpsStore.test.ts index 8ed194a1452..0eb94821a54 100644 --- a/internal-packages/run-store/src/runOpsStore.test.ts +++ b/internal-packages/run-store/src/runOpsStore.test.ts @@ -243,7 +243,7 @@ describe("RoutingRunStore (TaskRun-core)", () => { expect(await prisma17.taskRun.findUnique({ where: { id: bornId } })).not.toBeNull(); expect(await prisma14.taskRun.findUnique({ where: { id: bornId } })).toBeNull(); - // (ii) seed a cuid-length (LEGACY) row on the legacy DB and a ksuid-length (NEW) row on + // (ii) seed a cuid-length (LEGACY) row on the legacy DB and a run-ops id-length (NEW) row on // the new DB, then prove residency selection via ownerEngine length classification. const legacyRunId = `run_${CUID_25}`; const newRunId = `run_${NEW_ID_26}`; @@ -355,7 +355,7 @@ describe("RoutingRunStore (TaskRun-core)", () => { const seed = await seedEnvironment(prisma14, "d14"); - // Use a ksuid-length (NEW-residency) id to exercise the route; in single-DB both + // Use a run-ops id-length (NEW-residency) id to exercise the route; in single-DB both // slots are the same store, so the round-trip must succeed on the one client. const runId = `run_${NEW_ID_26}`; await router.createRun( @@ -536,19 +536,19 @@ describe("BatchTaskRun group", () => { ); heteroPostgresTest( - "findBatchTaskRunById routes ksuid→NEW and cuid→LEGACY", + "findBatchTaskRunById routes run-ops id→NEW and cuid→LEGACY", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed14 = await seedEnvironment(prisma14, "p8a_cuid14"); - const seed17 = await seedEnvironment(prisma17, "p8a_ksuid17"); + const seed17 = await seedEnvironment(prisma17, "p8a_runops17"); await newStore.createBatchTaskRun( batchCreateData({ id: NEW_ID_26, - friendlyId: "batch_ksuid_p8a", + friendlyId: "batch_runops_p8a", runtimeEnvironmentId: seed17.environment.id, runCount: 1, }) @@ -568,7 +568,7 @@ describe("BatchTaskRun group", () => { ); heteroPostgresTest( - "updateBatchTaskRun routes cuid→LEGACY and ksuid→NEW", + "updateBatchTaskRun routes cuid→LEGACY and run-ops id→NEW", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); @@ -577,13 +577,13 @@ describe("BatchTaskRun group", () => { const seed14 = await seedEnvironment(prisma14, "p8a_upd14"); const seed17 = await seedEnvironment(prisma17, "p8a_upd17"); - const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}u1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}u1`; const cuidBatchId = `${CUID_25.slice(0, -1)}u`; await newStore.createBatchTaskRun( batchCreateData({ - id: ksuidBatchId, - friendlyId: "batch_ksuid_upd", + id: runOpsBatchId, + friendlyId: "batch_runops_upd", runtimeEnvironmentId: seed17.environment.id, runCount: 2, }) @@ -598,12 +598,12 @@ describe("BatchTaskRun group", () => { ); const updNew = await router.updateBatchTaskRun({ - where: { id: ksuidBatchId }, + where: { id: runOpsBatchId }, data: { processingJobsCount: { increment: 1 } }, select: { processingJobsCount: true, runCount: true }, }); expect(updNew).toEqual({ processingJobsCount: 1, runCount: 2 }); - expect(await prisma14.batchTaskRun.findUnique({ where: { id: ksuidBatchId } })).toBeNull(); + expect(await prisma14.batchTaskRun.findUnique({ where: { id: runOpsBatchId } })).toBeNull(); const updLegacy = await router.updateBatchTaskRun({ where: { id: cuidBatchId }, @@ -623,11 +623,11 @@ describe("BatchTaskRun group", () => { const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed17 = await seedEnvironment(prisma17, "p8a_inc17"); - const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}i1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}i1`; await newStore.createBatchTaskRun( batchCreateData({ - id: ksuidBatchId, + id: runOpsBatchId, friendlyId: "batch_inc_p8a", runtimeEnvironmentId: seed17.environment.id, runCount: 1, @@ -659,10 +659,10 @@ describe("BatchTaskRun group", () => { }, }); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runId, status: "PENDING" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runId, status: "PENDING" }, }); - const withItems = await router.findBatchTaskRunById(ksuidBatchId, { + const withItems = await router.findBatchTaskRunById(runOpsBatchId, { include: { items: true }, }); expect(withItems?.items).toBeDefined(); @@ -672,30 +672,30 @@ describe("BatchTaskRun group", () => { ); heteroPostgresTest( - "createBatchTaskRun routes ksuid→NEW and cuid→LEGACY", + "createBatchTaskRun routes run-ops id→NEW and cuid→LEGACY", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed14 = await seedEnvironment(prisma14, "p8c_cuid14"); - const seed17 = await seedEnvironment(prisma17, "p8c_ksuid17"); + const seed17 = await seedEnvironment(prisma17, "p8c_runops17"); - const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}c1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}c1`; const cuidBatchId = `${CUID_25.slice(0, -2)}c1`; await router.createBatchTaskRun( batchCreateData({ - id: ksuidBatchId, - friendlyId: "batch_p8c_ksuid", + id: runOpsBatchId, + friendlyId: "batch_p8c_runops", runtimeEnvironmentId: seed17.environment.id, runCount: 1, }) ); expect( - await prisma17.batchTaskRun.findUnique({ where: { id: ksuidBatchId } }) + await prisma17.batchTaskRun.findUnique({ where: { id: runOpsBatchId } }) ).not.toBeNull(); - expect(await prisma14.batchTaskRun.findUnique({ where: { id: ksuidBatchId } })).toBeNull(); + expect(await prisma14.batchTaskRun.findUnique({ where: { id: runOpsBatchId } })).toBeNull(); await router.createBatchTaskRun( batchCreateData({ @@ -710,10 +710,10 @@ describe("BatchTaskRun group", () => { } ); - // Probe: a ksuid-id batch physically resident on LEGACY (written by batchTriggerV3 raw - // to the control-plane) must be found; strict id-routing (ksuid→NEW only) would miss it. + // Probe: a run-ops-id batch physically resident on LEGACY (written by batchTriggerV3 raw + // to the control-plane) must be found; strict id-routing (run-ops id→NEW only) would miss it. heteroPostgresTest( - "findBatchTaskRunById probe finds ksuid-id batch resident on LEGACY (cross-residency)", + "findBatchTaskRunById probe finds run-ops-id batch resident on LEGACY (cross-residency)", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); @@ -722,36 +722,38 @@ describe("BatchTaskRun group", () => { const seed14 = await seedEnvironment(prisma14, "p8c_probe14"); const seed17 = await seedEnvironment(prisma17, "p8c_probe17"); - const ksuidOnLegacy = `${NEW_ID_26.slice(0, -2)}l1`; + const runOpsIdOnLegacy = `${NEW_ID_26.slice(0, -2)}l1`; await legacyStore.createBatchTaskRun( batchCreateData({ - id: ksuidOnLegacy, + id: runOpsIdOnLegacy, friendlyId: "batch_p8c_probe_legacy", runtimeEnvironmentId: seed14.environment.id, runCount: 1, }) ); expect( - await prisma14.batchTaskRun.findUnique({ where: { id: ksuidOnLegacy } }) + await prisma14.batchTaskRun.findUnique({ where: { id: runOpsIdOnLegacy } }) ).not.toBeNull(); - expect(await prisma17.batchTaskRun.findUnique({ where: { id: ksuidOnLegacy } })).toBeNull(); + expect( + await prisma17.batchTaskRun.findUnique({ where: { id: runOpsIdOnLegacy } }) + ).toBeNull(); - expect((await router.findBatchTaskRunById(ksuidOnLegacy))?.id).toBe(ksuidOnLegacy); + expect((await router.findBatchTaskRunById(runOpsIdOnLegacy))?.id).toBe(runOpsIdOnLegacy); - const ksuidOnNew = `${NEW_ID_26.slice(0, -2)}n1`; + const runOpsIdOnNew = `${NEW_ID_26.slice(0, -2)}n1`; await newStore.createBatchTaskRun( batchCreateData({ - id: ksuidOnNew, + id: runOpsIdOnNew, friendlyId: "batch_p8c_probe_new", runtimeEnvironmentId: seed17.environment.id, runCount: 1, }) ); - expect((await router.findBatchTaskRunById(ksuidOnNew))?.id).toBe(ksuidOnNew); + expect((await router.findBatchTaskRunById(runOpsIdOnNew))?.id).toBe(runOpsIdOnNew); } ); - // A BATCH-completion waitpoint (cuid own-id, `completedByBatchId` = ksuid batch on NEW) must be + // A BATCH-completion waitpoint (cuid own-id, `completedByBatchId` = run-ops batch on NEW) must be // born on NEW alongside its batch. On the control-plane DB (prisma14) the Waitpoint→BatchTaskRun // FK is enforced, so routing by the waitpoint's own cuid id-shape would land it on LEGACY and // FK-violate against the absent batch. The dedicated run-ops schema carries `completedByBatchId` as a scalar. @@ -765,7 +767,7 @@ describe("BatchTaskRun group", () => { }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - // The ksuid batch lives on NEW only — never on the control-plane DB. + // The run-ops batch lives on NEW only — never on the control-plane DB. const batchId = `${NEW_ID_26.slice(0, -2)}b1`; await prisma17.batchTaskRun.create({ data: { @@ -814,7 +816,7 @@ describe("BatchTaskRun group", () => { describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () => { // A create routed to NEW must land on NEW even when the caller forwards the LEGACY // client as `tx` (the webapp passes its control-plane client there). If the router - // forwarded it, the ksuid run would be written through the legacy connection. + // forwarded it, the run-ops run would be written through the legacy connection. heteroPostgresTest( "createRun ignores a forwarded wrong-DB tx and lands the run on its owning store", async ({ prisma14, prisma17 }) => { @@ -923,7 +925,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { // Internal-id convention (matches the file): `run_` + a 25-char body (cuid → LEGACY) or - // a v1 body (ksuid → NEW). The classifier strips `run_` then keys on the version char. + // a v1 body (run-ops id → NEW). The classifier strips `run_` then keys on the version char. const legacyId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; const newId = (suffix: string) => `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; @@ -1258,7 +1260,7 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { } ); - // A waitpoint must be born on the same DB as its run (cuid → LEGACY, ksuid → NEW) so that + // A waitpoint must be born on the same DB as its run (cuid → LEGACY, run-ops id → NEW) so that // completion and the blocking edge — which already routes by run id — line up. A cuid // waitpoint landing on NEW is the regression that strands a non-opted org's wait forever. heteroPostgresTest( @@ -1285,10 +1287,10 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { expect(await prisma14.waitpoint.findUnique({ where: { id: cuidWp } })).not.toBeNull(); expect(await prisma17.waitpoint.findUnique({ where: { id: cuidWp } })).toBeNull(); - const ksuidWp = `waitpoint_${NEW_ID_26}`; + const runOpsWp = `waitpoint_${NEW_ID_26}`; await router.createWaitpoint({ data: { - id: ksuidWp, + id: runOpsWp, friendlyId: "waitpoint_co_k", type: "MANUAL", idempotencyKey: "co-key-k", @@ -1297,8 +1299,8 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { environmentId: seed17.environment.id, }, }); - expect(await prisma17.waitpoint.findUnique({ where: { id: ksuidWp } })).not.toBeNull(); - expect(await prisma14.waitpoint.findUnique({ where: { id: ksuidWp } })).toBeNull(); + expect(await prisma17.waitpoint.findUnique({ where: { id: runOpsWp } })).not.toBeNull(); + expect(await prisma14.waitpoint.findUnique({ where: { id: runOpsWp } })).toBeNull(); } ); }); @@ -1336,10 +1338,10 @@ describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () = // NEW side has no control-plane tables and no associatedWaitpoint relation; // seed the TaskRun row directly with synthetic scalar ids. - const ksuidId = newId("t10"); + const runOpsId = newId("t10"); await prisma17.taskRun.create({ data: { - id: ksuidId, + id: runOpsId, engine: "V2", status: "PENDING", friendlyId: "run_t10_new", @@ -1364,11 +1366,11 @@ describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () = }); const rows = (await router.findRuns({ - where: { id: { in: [cuidId, ksuidId] } }, + where: { id: { in: [cuidId, runOpsId] } }, select: { id: true }, })) as Array<{ id: string }>; - expect(rows.map((r) => r.id).sort()).toEqual([cuidId, ksuidId].sort()); + expect(rows.map((r) => r.id).sort()).toEqual([cuidId, runOpsId].sort()); }, 120_000 ); @@ -1499,7 +1501,7 @@ describe("RoutingRunStore write-path fan-outs", () => { } ); - // expireRunsBatch with mixed ksuid+cuid ids partitions across both DBs and sums. + // expireRunsBatch with mixed run-ops id+cuid ids partitions across both DBs and sums. heteroPostgresTest( "expireRunsBatch with mixed ids partitions across NEW+LEGACY and sums count", async ({ prisma14, prisma17 }) => { @@ -1548,9 +1550,9 @@ describe("RoutingRunStore write-path fan-outs", () => { } ); - // all-ksuid batch goes only to NEW; LEGACY store is not called with an empty list. + // all-run-ops batch goes only to NEW; LEGACY store is not called with an empty list. heteroPostgresTest( - "expireRunsBatch all-ksuid batch skips LEGACY (no empty IN query)", + "expireRunsBatch all-run-ops batch skips LEGACY (no empty IN query)", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); let legacyCalled = false; @@ -1574,7 +1576,7 @@ describe("RoutingRunStore write-path fan-outs", () => { buildCreateRunInput({ runId: nId, friendlyId: "run_ks_n", - taskIdentifier: "ksuid-only-task", + taskIdentifier: "runops-only-task", organizationId: seed17.organization.id, projectId: seed17.project.id, runtimeEnvironmentId: seed17.environment.id, @@ -1662,13 +1664,13 @@ describe("RoutingRunStore.findTaskRunAttempt residency routing", () => { ); heteroPostgresTest( - "a ksuid (NEW) run's attempt still resolves via findTaskRunAttempt", + "a run-ops id (NEW) run's attempt still resolves via findTaskRunAttempt", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const seed17 = await seedEnvironment(prisma17, "t9a_ksuid17"); + const seed17 = await seedEnvironment(prisma17, "t9a_runops17"); const runId = newRunId("t9a2"); await newStore.createRun( buildCreateRunInput({ @@ -1681,7 +1683,7 @@ describe("RoutingRunStore.findTaskRunAttempt residency routing", () => { }) ); - const attemptId = "attempt_t9a_ksuid1"; + const attemptId = "attempt_t9a_runops1"; await seedAttempt(prisma17, { attemptId, friendlyId: "attempt_t9a_k1", @@ -1865,7 +1867,7 @@ describe("findBatchTaskRunByFriendlyId probe", () => { ); }); -// Batch residency: the four new accessors must route by batch id so a ksuid +// Batch residency: the four new accessors must route by batch id so a run-ops id // batch + its items live on NEW with its child runs, and fall back to fan-out where there // is no classifiable id (idempotency probe; status-only updateMany). describe("RoutingRunStore batch-residency accessors", () => { @@ -1931,7 +1933,7 @@ describe("RoutingRunStore batch-residency accessors", () => { const seed14 = await seedEnvironment(prisma14, "optA_idem14"); - // ksuid batch with an idempotency key on NEW (dedicated, scalar env id) + // run-ops batch with an idempotency key on NEW (dedicated, scalar env id) await newStore.createBatchTaskRun( batchData({ id: `${NEW_ID_26.slice(0, -2)}i1`, @@ -1962,7 +1964,7 @@ describe("RoutingRunStore batch-residency accessors", () => { } ); - // updateManyBatchTaskRun: routes by where.id (ksuid→NEW, cuid→LEGACY); fans out + sums when unrouted. + // updateManyBatchTaskRun: routes by where.id (run-ops id→NEW, cuid→LEGACY); fans out + sums when unrouted. heteroRunOpsPostgresTest( "updateManyBatchTaskRun routes by where.id and fans out otherwise", async ({ prisma14, prisma17 }) => { @@ -1972,10 +1974,10 @@ describe("RoutingRunStore batch-residency accessors", () => { const seed14 = await seedEnvironment(prisma14, "optA_um14"); - const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}m1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}m1`; const cuidBatchId = `${CUID_25.slice(0, -2)}m1`; await newStore.createBatchTaskRun( - batchData({ id: ksuidBatchId, friendlyId: "batch_um_new", runtimeEnvironmentId: ENV_NEW }) + batchData({ id: runOpsBatchId, friendlyId: "batch_um_new", runtimeEnvironmentId: ENV_NEW }) ); await legacyStore.createBatchTaskRun( batchData({ @@ -1985,14 +1987,14 @@ describe("RoutingRunStore batch-residency accessors", () => { }) ); - // where.id ksuid → NEW only + // where.id run-ops id → NEW only const upNew = await router.updateManyBatchTaskRun({ - where: { id: ksuidBatchId }, + where: { id: runOpsBatchId }, data: { status: "COMPLETED" }, }); expect(upNew.count).toBe(1); expect( - (await prisma17.batchTaskRun.findUnique({ where: { id: ksuidBatchId } }))?.status + (await prisma17.batchTaskRun.findUnique({ where: { id: runOpsBatchId } }))?.status ).toBe("COMPLETED"); // where.id cuid → LEGACY only @@ -2022,9 +2024,9 @@ describe("RoutingRunStore batch-residency accessors", () => { const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}c1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}c1`; await newStore.createBatchTaskRun( - batchData({ id: ksuidBatchId, friendlyId: "batch_cnt_new", runtimeEnvironmentId: ENV_NEW }) + batchData({ id: runOpsBatchId, friendlyId: "batch_cnt_new", runtimeEnvironmentId: ENV_NEW }) ); const runA = `${NEW_ID_26.slice(0, -3)}ra1`; @@ -2032,15 +2034,15 @@ describe("RoutingRunStore batch-residency accessors", () => { await seedDedicatedRun(prisma17, ENV_NEW, runA); await seedDedicatedRun(prisma17, ENV_NEW, runB); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runA, status: "COMPLETED" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runA, status: "COMPLETED" }, }); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runB, status: "PENDING" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runB, status: "PENDING" }, }); - expect(await router.countBatchTaskRunItems({ batchTaskRunId: ksuidBatchId })).toBe(2); + expect(await router.countBatchTaskRunItems({ batchTaskRunId: runOpsBatchId })).toBe(2); expect( - await router.countBatchTaskRunItems({ batchTaskRunId: ksuidBatchId, status: "COMPLETED" }) + await router.countBatchTaskRunItems({ batchTaskRunId: runOpsBatchId, status: "COMPLETED" }) ).toBe(1); } ); @@ -2053,24 +2055,24 @@ describe("RoutingRunStore batch-residency accessors", () => { const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const ksuidBatchId = `${NEW_ID_26.slice(0, -2)}u1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}u1`; await newStore.createBatchTaskRun( - batchData({ id: ksuidBatchId, friendlyId: "batch_ui_new", runtimeEnvironmentId: ENV_NEW }) + batchData({ id: runOpsBatchId, friendlyId: "batch_ui_new", runtimeEnvironmentId: ENV_NEW }) ); const runX = `${NEW_ID_26.slice(0, -3)}ux1`; await seedDedicatedRun(prisma17, ENV_NEW, runX); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runX, status: "PENDING" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runX, status: "PENDING" }, }); const res = await router.updateManyBatchTaskRunItems({ - where: { batchTaskRunId: ksuidBatchId, taskRunId: runX }, + where: { batchTaskRunId: runOpsBatchId, taskRunId: runX }, data: { status: "COMPLETED" }, }); expect(res.count).toBe(1); const item = await prisma17.batchTaskRunItem.findFirst({ - where: { batchTaskRunId: ksuidBatchId, taskRunId: runX }, + where: { batchTaskRunId: runOpsBatchId, taskRunId: runX }, }); expect(item?.status).toBe("COMPLETED"); } diff --git a/internal-packages/run-store/src/runOpsStore.ts b/internal-packages/run-store/src/runOpsStore.ts index f218f1d6ba0..dde120dab60 100644 --- a/internal-packages/run-store/src/runOpsStore.ts +++ b/internal-packages/run-store/src/runOpsStore.ts @@ -31,7 +31,7 @@ import type { * Run-ops routing substrate for the TaskRun-core method group. Implements {@link RunStore} * by selecting between a NEW store (the dedicated run-ops DB, where new runs are born) and * a LEGACY store (the control-plane DB) via the residency classifier (`ownerEngine`: - * ksuid→NEW, cuid→LEGACY). In single-DB both stores are the same, so routing is a no-op + * run-ops id→NEW, cuid→LEGACY). In single-DB both stores are the same, so routing is a no-op * passthrough. Inert until the injecting seam wires it in under `isSplitEnabled()`; reads no * flag here. The TaskRun-core methods (create/find/findRuns + updateMetadata/clearIdempotencyKey) * route by residency; all other methods are mechanical residency-routing delegates. @@ -83,7 +83,7 @@ export class RoutingRunStore implements RunStore { } } - // WRITE routing is pure id-shape (cuid → LEGACY, ksuid → NEW). A LEGACY-classified id is + // WRITE routing is pure id-shape (cuid → LEGACY, run-ops id → NEW). A LEGACY-classified id is // always LEGACY-resident; no marker check exists. Kept async so the many // `await this.#routeForWrite(...)` call sites need no edits (awaiting a resolved store is // a no-op). @@ -108,7 +108,7 @@ export class RoutingRunStore implements RunStore { return this.#routeOrNew(runId).runInTransaction(runId, fn); } - // A waitpoint WRITE co-locates with its run by id-shape (cuid → LEGACY, ksuid → NEW, + // A waitpoint WRITE co-locates with its run by id-shape (cuid → LEGACY, run-ops id → NEW, // unclassifiable → LEGACY), mirroring how `blockRunWithWaitpointEdges` routes the edge by // run id. `tx` is forwarded only to LEGACY (same physical DB as the control-plane tx); // for NEW it's dropped so the row lands on NEW's own client. @@ -143,7 +143,7 @@ export class RoutingRunStore implements RunStore { // --------------------------------------------------------------------------- // TaskRun-core: Create — a run is born on the store named by its MINTED id-kind: - // cuid → LEGACY, ksuid → NEW, unclassifiable → NEW. The mint layer encodes + // cuid → LEGACY, run-ops id → NEW, unclassifiable → NEW. The mint layer encodes // inherited residency into the id-kind, so create-by-id-shape is correct; // a brand-new run has no redirect marker. // @@ -298,7 +298,7 @@ export class RoutingRunStore implements RunStore { } // Bounded id-set (the list hydrate + engine sweeps). Query NEW for the whole set first - // (it holds ksuid runs); probe LEGACY only for the ids NEW missed that could still live + // (it holds run-ops runs); probe LEGACY only for the ids NEW missed that could still live // there (cuid). The two id sets are disjoint by construction, so the merge needs no dedupe. async #findRunsByIdSet(args: FindRunsArgs, ids: string[]): Promise { const { args: selArgs, addedFields } = ensureProjected(args); @@ -315,7 +315,7 @@ export class RoutingRunStore implements RunStore { const toLegacy: string[] = []; for (const id of ids) { if (foundIds.has(id)) continue; - if (this.#classifySafe(id) === "NEW") continue; // ksuid: cannot live on LEGACY + if (this.#classifySafe(id) === "NEW") continue; // run-ops id: cannot live on LEGACY toLegacy.push(id); } @@ -475,7 +475,7 @@ export class RoutingRunStore implements RunStore { data: { error: TaskRunError; now: Date }, tx?: PrismaClientOrTransaction ): Promise { - // Partition by id-shape: ksuid → NEW, everything else → LEGACY. Call each store + // Partition by id-shape: run-ops id → NEW, everything else → LEGACY. Call each store // only when its partition is non-empty (avoids an empty IN () clause). Sum counts. const newIds = runIds.filter((id) => this.#classifySafe(id) === "NEW"); const legacyIds = runIds.filter((id) => this.#classifySafe(id) !== "NEW"); @@ -705,7 +705,7 @@ export class RoutingRunStore implements RunStore { // completed the run; the owning store can only hydrate the ones that live on its own DB. When every // join id is already present we leave the array untouched (byte-identical for single-DB / the // co-resident steady state — no extra fan-out write); only genuinely-missing ids are resolved - // cross-DB and appended, so a cuid token completing a ksuid run keeps its OUTPUT on the resume. + // cross-DB and appended, so a cuid token completing a run-ops run keeps its OUTPUT on the resume. async #reresolveCompletedWaitpointsCrossDb( snapshot: Record, owningStore: RunStore @@ -1083,7 +1083,7 @@ export class RoutingRunStore implements RunStore { // strand that run forever. Dedup is a no-op in steady state; it guards the copy→fence window. // // The edge's `waitpoint`/`taskRun` relations can also straddle DBs (a cuid MANUAL/DATETIME token - // blocking a ksuid run; a drain-relocated token). A single store hydrates them from its own + // blocking a run-ops run; a drain-relocated token). A single store hydrates them from its own // client only → a cross-DB target resolves to null → the run hangs or its resume // output is silently dropped. So the router strips those relation keys from the per-leg // query (scalar edges only) and re-resolves them across BOTH stores here. @@ -1212,16 +1212,16 @@ export class RoutingRunStore implements RunStore { } // --------------------------------------------------------------------------- - // BatchTaskRun (run-ops). Route by id-shape: ksuid→NEW, cuid→LEGACY. + // BatchTaskRun (run-ops). Route by id-shape: run-ops id→NEW, cuid→LEGACY. // --------------------------------------------------------------------------- async createBatchTaskRun( data: CreateBatchTaskRunData, tx?: PrismaClientOrTransaction ): Promise { - // Route by the batch's classifiable internal id: ksuid→NEW, cuid→LEGACY. + // Route by the batch's classifiable internal id: run-ops id→NEW, cuid→LEGACY. // Never forward a control-plane tx to NEW (the create would land in the wrong DB, stranding the - // ksuid batch + its co-resident child runs/items); forward tx only to LEGACY (same physical DB + // run-ops batch + its co-resident child runs/items); forward tx only to LEGACY (same physical DB // as the tx). Mirrors #routeWaitpointWrite / updateBatchTaskRun. const store = await this.#routeOrNewForWrite(data.id); return store.createBatchTaskRun(data, store === this.#legacy ? tx : undefined); @@ -1245,7 +1245,7 @@ export class RoutingRunStore implements RunStore { // Batches can be written to either DB by different create paths (runEngine routes by id; // batchTriggerV3 writes raw to the control-plane), so probe NEW first then LEGACY rather - // than strict id-routing, which would miss a ksuid-id batch resident on the control-plane. + // than strict id-routing, which would miss a run-ops-id batch resident on the control-plane. async findBatchTaskRunById( id: string, args?: { include?: T }, @@ -1272,7 +1272,7 @@ export class RoutingRunStore implements RunStore { } // --------------------------------------------------------------------------- - // Batch residency — route every batch op by the batch id so a ksuid + // Batch residency — route every batch op by the batch id so a run-ops id // batch + its items co-reside on NEW with its child runs (the TaskRun.batchId and // BatchTaskRunItem.batchTaskRunId FKs resolve locally). // --------------------------------------------------------------------------- @@ -1316,7 +1316,7 @@ export class RoutingRunStore implements RunStore { where: { batchTaskRunId: string; status?: BatchTaskRunItemStatus }, client?: ReadClient ): Promise { - // Never forward the caller's client (it is the control-plane handle): a ksuid batch routes to + // Never forward the caller's client (it is the control-plane handle): a run-ops batch routes to // NEW, so a forwarded control-plane client would count items on the wrong DB (→ 0/wrong count). // The routed store reads its OWN DB. Mirrors the probe-read sweep. return this.#routeOrNew(where.batchTaskRunId).countBatchTaskRunItems(where); diff --git a/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts b/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts index 25eb894a88e..3f9b345e43c 100644 --- a/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts +++ b/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts @@ -20,7 +20,7 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by the version char after stripping a single leading `_`: a v1 body -// → ksuid → NEW (#new / dedicated subset), 25 chars → cuid → LEGACY (#legacy / full schema). +// → run-ops id → NEW (#new / dedicated subset), 25 chars → cuid → LEGACY (#legacy / full schema). const NEW_ID_26 = "k".repeat(24) + "01"; const CUID_25 = "c".repeat(25); @@ -490,7 +490,7 @@ describe("RunStore run-ops persistence — waitpoints", () => { } ); - // the silent-hang case, against the REAL split. A NEW (ksuid) run is blocked on + // the silent-hang case, against the REAL split. A NEW (run-ops id) run is blocked on // a LEGACY (cuid) token, so its block edge lives on #new (co-located with the run) while the token's // id-shape says LEGACY. Completing that token must FAN OUT the waitpointId edge read across both DBs // and find the edge on #new — routing by the token's id-shape (LEGACY) returns zero edges and the @@ -505,7 +505,7 @@ describe("RunStore run-ops persistence — waitpoints", () => { // The NEW run + its (synthetic) env live on the dedicated #new subset (prisma17). const env17 = await seedEnvironment(prisma17, "dedicated", "we17"); - const runId = `run_${NEW_ID_26}`; // ksuid → NEW residency + const runId = `run_${NEW_ID_26}`; // run-ops id → NEW residency await router.createRun( buildCreateRunInput({ runId, diff --git a/internal-packages/run-store/src/types.ts b/internal-packages/run-store/src/types.ts index 32e5644114f..be99f22a464 100644 --- a/internal-packages/run-store/src/types.ts +++ b/internal-packages/run-store/src/types.ts @@ -310,7 +310,7 @@ export interface RunStore { /** * Run a co-resident multi-write unit atomically on the store that OWNS `runId`. The callback gets * the owning `RunStore` plus a `tx` opened on THAT store's OWN client; passing `tx` to the inner - * writes lands them all in ONE transaction on the owning DB (NEW for a ksuid run, LEGACY for a cuid + * writes lands them all in ONE transaction on the owning DB (NEW for a run-ops run, LEGACY for a cuid * run), so a failure between two writes rolls BOTH back. NOT a cross-DB transaction: `tx` is the * owning store's own client (never the control-plane tx), and every write MUST target the same run / * its co-resident subgraph. Callers MUST use the supplied `store` + `tx`, not the outer router From 40fca0458e8d04e7aa03d40b29a0aa96f546b5c2 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 00:44:44 +0100 Subject: [PATCH 3/9] docs(run-ops): note the residency-classifier transition/deploy-ordering constraint Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/v3/isomorphic/runOpsResidency.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.ts b/packages/core/src/v3/isomorphic/runOpsResidency.ts index 1aa4c335c93..2e960548174 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.ts @@ -45,7 +45,9 @@ function internalForm(id: string): string { * Returns the id lineage by the version-char rule: a well-formed run-ops v1 * body (26 chars, version "1" at index 25, base32hex alphabet) is "ksuid" * (NEW store); everything else — including malformed v1 shapes — is "cuid" - * (legacy). Total: never throws. + * (legacy). Total: never throws. Transition: pre-cutover 27-char base62 KSUIDs (the old + * NEW-mint format) now classify LEGACY, so ship this with the base32hex generator only once + * any 27-char NEW-resident runs are drained/disposable — no live run is misrouted mid-cutover. */ export function classifyKind(id: string): ResidencyKind { return isRunOpsIdBody(internalForm(id)) ? "ksuid" : "cuid"; From 95fbc44f76377490b63ab52dfee79d3bb83b0e4a Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 00:56:05 +0100 Subject: [PATCH 4/9] test(run-store): seed the NEW-arm routed-read test with a v1 run-ops id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The routed-read primary test (from #4153) hardcoded a 27-char KSUID as the NEW-resident id, which the base32hex version-char discriminator now classifies LEGACY — so the NEW arm routed to the wrong store and failed. Seed it with a real generateRunOpsId() v1 body instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/runOpsStore.routedReadPrimary.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts b/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts index 4e9b191de55..8d2333cf81c 100644 --- a/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts +++ b/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts @@ -12,6 +12,7 @@ // primary-routed read finds the row, with no replica==primary aliasing to mask the drop. import { heteroPostgresTest } from "@internal/testcontainers"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClient } from "@trigger.dev/database"; import { describe, expect } from "vitest"; import { PostgresRunStore } from "./PostgresRunStore.js"; @@ -19,9 +20,10 @@ import { RoutingRunStore } from "./runOpsStore.js"; import { markReadReplicaClient } from "./readReplicaClient.js"; import type { CreateRunInput } from "./types.js"; -// ownerEngine classifies by internal-id LENGTH: 25 chars → cuid → LEGACY, 27 → ksuid → NEW. +// ownerEngine classifies by the version char: a 25-char cuid → LEGACY; a valid run-ops v1 body +// (26 chars: base32hex core + region char + version "1") → NEW. const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const RUN_OPS_ID_BODY = generateRunOpsId(); // Router topology where the OWNING store (the one the test's run ids route to) writes to `writer` // but reads by default from `lagging` — a physically separate, never-written DB. The other store @@ -189,16 +191,16 @@ describe("run-ops split — routed reads honor a caller-passed client via the ow } ); - // NEW (ksuid) routing arm. The caller's client here is the CONTROL-PLANE writer — the wrong + // NEW (run-ops id) routing arm. The caller's client here is the CONTROL-PLANE writer — the wrong // physical DB for a NEW-resident run — so this also pins that the client is never forwarded // verbatim: the read must resolve on the owning NEW store's OWN primary. heteroPostgresTest( - "NEW ksuid: a control-plane client routes the snapshot read to the NEW store's OWN primary", + "NEW run-ops id: a control-plane client routes the snapshot read to the NEW store's OWN primary", async ({ prisma14, prisma17 }) => { // Owning (NEW) store writes to prisma14; the control-plane/other store is prisma17. const { router } = splitTopology("NEW", prisma14, prisma17); const seed = await seedEnvironment(prisma14, "snap_new"); - const runId = `run_${KSUID_27}`; + const runId = `run_${RUN_OPS_ID_BODY}`; await router.createRun( buildCreateRunInput({ runId, From 163f1ed18516a905751c869fd410bc2fa0c438e1 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 01:20:48 +0100 Subject: [PATCH 5/9] chore(run-ops): rename the "ksuid" mint-kind value to "runOpsId" Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/olive-bees-repeat.md | 2 +- apps/webapp/app/env.server.ts | 2 +- .../idempotencyResidency.server.test.ts | 6 ++--- .../concerns/idempotencyResidency.server.ts | 4 ++-- .../services/triggerFailedTask.server.ts | 2 +- .../runEngine/services/triggerTask.server.ts | 2 +- apps/webapp/app/v3/featureFlags.ts | 2 +- .../mintBatchFriendlyId.server.test.ts | 24 +++++++++---------- .../mintBatchFriendlyId.server.ts | 2 +- .../resolveInheritedMintKind.server.test.ts | 4 ++-- .../resolveInheritedMintKind.server.ts | 2 +- .../runOpsMintKind.flipLatency.test.ts | 18 +++++++------- .../runOpsMintKind.server.test.ts | 14 +++++------ .../runOpsMigration/runOpsMintKind.server.ts | 4 ++-- .../app/v3/services/batchTriggerV3.server.ts | 2 +- ...batchTriggerV3ResidencyInheritance.test.ts | 4 ++-- .../test/engine/triggerFailedTask.test.ts | 6 ++--- apps/webapp/test/engine/triggerTask.test.ts | 6 ++--- apps/webapp/test/runOpsMintCutover.test.ts | 18 +++++++------- .../src/v3/isomorphic/runOpsResidency.test.ts | 2 +- .../core/src/v3/isomorphic/runOpsResidency.ts | 16 ++++++------- 21 files changed, 71 insertions(+), 71 deletions(-) diff --git a/.changeset/olive-bees-repeat.md b/.changeset/olive-bees-repeat.md index 6e3e3572780..3702f284f51 100644 --- a/.changeset/olive-bees-repeat.md +++ b/.changeset/olive-bees-repeat.md @@ -2,4 +2,4 @@ "@trigger.dev/core": patch --- -Change the run-ops run id format from a 27-char base62 KSUID to a 26-char lowercase base32hex id (ms timestamp + CSPRNG core, region char, version char) so run ids are DNS-1123 safe for Kubernetes pod names while staying lexicographically time-sortable +Change the run-ops run id format from a 27-char base62 KSUID to a 26-char lowercase base32hex id (ms timestamp + CSPRNG core, region char, version char) so run ids are DNS-1123 safe for Kubernetes pod names while staying lexicographically time-sortable, and rename the exported `ResidencyKind` value `"ksuid"` to `"runOpsId"` to reflect that the NEW-store id is no longer a KSUID diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index ed254be5f82..af43515d179 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -1729,7 +1729,7 @@ const EnvironmentSchema = z // Run-ops id mint cutover — per-env, canary-first, OFF by default. // Even when on, an env mints run-ops ids only if its per-org runOpsMintKsuid flag is - // "ksuid" AND isSplitEnabled() is true. Cache mirrors REALTIME_BACKEND_FLAG_CACHE_*. + // "runOpsId" AND isSplitEnabled() is true. Cache mirrors REALTIME_BACKEND_FLAG_CACHE_*. RUN_OPS_MINT_KSUID_ENABLED: BoolEnv.default(false), RUN_OPS_MINT_FLAG_CACHE_TTL_MS: z.coerce.number().int().default(30_000), RUN_OPS_MINT_FLAG_CACHE_MAX_ENTRIES: z.coerce.number().int().default(10_000), diff --git a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts index 8bb37adf541..39b806a0f71 100644 --- a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts +++ b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts @@ -16,7 +16,7 @@ function makeDeps(over: Partial): ResolveIdempoten fallbackClient: FALLBACK, newClient: NEW_CLIENT, legacyClient: LEGACY_CLIENT, - resolveMintKind: async () => "ksuid", + resolveMintKind: async () => "runOpsId", classify: (id) => { if (id.length === 26 && id[25] === "1") return "NEW"; if (id.length === 25) return "LEGACY"; @@ -41,7 +41,7 @@ describe("resolveIdempotencyDedupClient", () => { it("routes a root run to the NEW client when the env mints run-ops ids", async () => { const client = await resolveIdempotencyDedupClient( { environmentForMint: env, parentRunFriendlyId: undefined }, - makeDeps({ resolveMintKind: async () => "ksuid" }) + makeDeps({ resolveMintKind: async () => "runOpsId" }) ); expect(client).toBe(NEW_CLIENT); }); @@ -67,7 +67,7 @@ describe("resolveIdempotencyDedupClient", () => { const cuidParent = RunId.toFriendlyId("b".repeat(25)); const client = await resolveIdempotencyDedupClient( { environmentForMint: env, parentRunFriendlyId: cuidParent }, - makeDeps({ resolveMintKind: async () => "ksuid" }) // mint flag must NOT win for a child + makeDeps({ resolveMintKind: async () => "runOpsId" }) // mint flag must NOT win for a child ); expect(client).toBe(LEGACY_CLIENT); }); diff --git a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts index 38ef4755844..86f1435654b 100644 --- a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts +++ b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts @@ -1,7 +1,7 @@ import { ownerEngine, RunId, type Residency } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction } from "@trigger.dev/database"; -type MintKind = "cuid" | "ksuid"; +type MintKind = "cuid" | "runOpsId"; export type ResolveIdempotencyClientDeps = { isSplitEnabled: () => Promise; @@ -52,5 +52,5 @@ export async function resolveIdempotencyDedupClient( } const kind = await deps.resolveMintKind(args.environmentForMint); - return clientFor(kind === "ksuid" ? "NEW" : "LEGACY"); + return clientFor(kind === "runOpsId" ? "NEW" : "LEGACY"); } diff --git a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts index 29304079aed..ee420c835de 100644 --- a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts @@ -102,7 +102,7 @@ export class TriggerFailedTaskService { orgFeatureFlags: args.orgFeatureFlags, }); - return mintKind === "ksuid" + return mintKind === "runOpsId" ? RunId.toFriendlyId(generateRunOpsId()) : RunId.generate().friendlyId; } diff --git a/apps/webapp/app/runEngine/services/triggerTask.server.ts b/apps/webapp/app/runEngine/services/triggerTask.server.ts index e863013d82c..6a72ad34a18 100644 --- a/apps/webapp/app/runEngine/services/triggerTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerTask.server.ts @@ -153,7 +153,7 @@ export class RunEngineTriggerTaskService { orgFeatureFlags: environment.organization.featureFlags, }); - return mintKind === "ksuid" + return mintKind === "runOpsId" ? RunId.toFriendlyId(generateRunOpsId(region)) : RunId.generate().friendlyId; } diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index f85ba79e456..cdf29906354 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -53,7 +53,7 @@ export const FeatureFlagCatalog = { [FEATURE_FLAG.devBranchesEnabled]: z.coerce.boolean(), // Per-org run-ops-id mint cutover. Defaults to "cuid"; only honored when // RUN_OPS_MINT_KSUID_ENABLED is on AND isSplitEnabled() is true. - [FEATURE_FLAG.runOpsMintKsuid]: z.enum(["cuid", "ksuid"]), + [FEATURE_FLAG.runOpsMintKsuid]: z.enum(["cuid", "runOpsId"]), }; export type FeatureFlagKey = keyof typeof FeatureFlagCatalog; diff --git a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts index 168647af4f4..9973be57d1d 100644 --- a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts @@ -3,12 +3,12 @@ import { batchIdForMintKind, resolveBatchMintKind } from "./mintBatchFriendlyId. import { classifyKind } from "@trigger.dev/core/v3/isomorphic"; describe("batchIdForMintKind (pure)", () => { - it("'ksuid' kind -> 26-char classifiable NEW batch id (no 21-char ids)", () => { - const r = batchIdForMintKind("ksuid"); + it("'runOpsId' kind -> 26-char classifiable NEW batch id (no 21-char ids)", () => { + const r = batchIdForMintKind("runOpsId"); expect(r.friendlyId.startsWith("batch_")).toBe(true); expect(r.id.length).toBe(26); - expect(classifyKind(r.id)).toBe("ksuid"); - expect(classifyKind(r.friendlyId)).toBe("ksuid"); + expect(classifyKind(r.id)).toBe("runOpsId"); + expect(classifyKind(r.friendlyId)).toBe("runOpsId"); }); it("cuid -> 25-char classifiable LEGACY batch id", () => { @@ -19,7 +19,7 @@ describe("batchIdForMintKind (pure)", () => { }); it("never mints a 21-char id", () => { - for (const kind of ["cuid", "ksuid"] as const) { + for (const kind of ["cuid", "runOpsId"] as const) { expect([25, 26]).toContain(batchIdForMintKind(kind).id.length); } }); @@ -29,12 +29,12 @@ describe("resolveBatchMintKind", () => { const environment = { organizationId: "org_1", id: "env_1", orgFeatureFlags: {} }; it("ROOT batch (no parent) resolves per-org kind via resolveRunIdMintKind", async () => { - const resolveRunIdMintKind = vi.fn().mockResolvedValue("ksuid"); + const resolveRunIdMintKind = vi.fn().mockResolvedValue("runOpsId"); const kind = await resolveBatchMintKind({ environment, deps: { resolveRunIdMintKind }, }); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); expect(resolveRunIdMintKind).toHaveBeenCalledWith({ organizationId: "org_1", id: "env_1", @@ -61,7 +61,7 @@ describe("resolveBatchMintKind", () => { deps: { resolveRunIdMintKind }, }); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); expect(resolveRunIdMintKind).not.toHaveBeenCalled(); }); @@ -81,9 +81,9 @@ describe("resolveBatchMintKind", () => { // mint-on-FLIP invariant: a child follows its parent's store even after the org flag // flips the other way. The flag resolver must NEVER be consulted for a child. - it("FLIP 'cuid'->'ksuid': a cuid (LEGACY) parent still mints a cuid child though the flag now says 'ksuid'", async () => { + it("FLIP 'cuid'->'runOpsId': a cuid (LEGACY) parent still mints a cuid child though the flag now says 'runOpsId'", async () => { const parentRunFriendlyId = `run_${"a".repeat(25)}`; - const resolveRunIdMintKind = vi.fn().mockResolvedValue("ksuid"); // flag flipped to ksuid + const resolveRunIdMintKind = vi.fn().mockResolvedValue("runOpsId"); // flag flipped to runOpsId const kind = await resolveBatchMintKind({ environment, parentRunFriendlyId, @@ -93,7 +93,7 @@ describe("resolveBatchMintKind", () => { expect(resolveRunIdMintKind).not.toHaveBeenCalled(); }); - it("FLIP 'ksuid'->'cuid': a run-ops (NEW) parent still mints a run-ops child though the flag now says 'cuid'", async () => { + it("FLIP 'runOpsId'->'cuid': a run-ops (NEW) parent still mints a run-ops child though the flag now says 'cuid'", async () => { const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`; const resolveRunIdMintKind = vi.fn().mockResolvedValue("cuid"); // flag flipped back to cuid const kind = await resolveBatchMintKind({ @@ -101,7 +101,7 @@ describe("resolveBatchMintKind", () => { parentRunFriendlyId, deps: { resolveRunIdMintKind }, }); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); expect(resolveRunIdMintKind).not.toHaveBeenCalled(); }); }); diff --git a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts index 86e73532235..e2d8511e3ff 100644 --- a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts @@ -14,7 +14,7 @@ const defaultDeps: ResolveDeps = { }; export function batchIdForMintKind(kind: RunIdMintKind): { id: string; friendlyId: string } { - if (kind === "ksuid") { + if (kind === "runOpsId") { const id = generateRunOpsId(); return { id, friendlyId: BatchId.toFriendlyId(id) }; } diff --git a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts index b7f6e313e49..3f135793f84 100644 --- a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts @@ -5,8 +5,8 @@ const NEW_PARENT = `run_${"a".repeat(24) + "01"}`; // run-ops id-shape -> NEW const LEGACY_PARENT = `run_${"b".repeat(25)}`; // cuid id-shape -> LEGACY describe("resolveInheritedMintKind (pure id-shape, shared across all mint paths)", () => { - it("inherits a run-ops (NEW) parent by id-shape -> 'ksuid' kind", () => { - expect(resolveInheritedMintKind(NEW_PARENT)).toBe("ksuid"); + it("inherits a run-ops (NEW) parent by id-shape -> 'runOpsId' kind", () => { + expect(resolveInheritedMintKind(NEW_PARENT)).toBe("runOpsId"); }); it("inherits a cuid (LEGACY) parent by id-shape -> cuid", () => { diff --git a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts index 63d689eae39..6ec9583c94b 100644 --- a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts @@ -6,5 +6,5 @@ import type { RunIdMintKind } from "./runOpsMintKind.server"; // flip. With no migration/drain, residency is a pure id-shape check (zero hot-path // I/O): a run-ops (NEW) parent mints run-ops children, a cuid (LEGACY) parent mints cuid. export function resolveInheritedMintKind(parentRunFriendlyId: string): RunIdMintKind { - return ownerEngine(parentRunFriendlyId) === "NEW" ? "ksuid" : "cuid"; + return ownerEngine(parentRunFriendlyId) === "NEW" ? "runOpsId" : "cuid"; } diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts index 07ead53aad0..0465c8d1c8b 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts @@ -31,7 +31,7 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window — current be beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); - it("returns the STALE cached kind within the TTL after the flag flips 'cuid'->'ksuid'", async () => { + it("returns the STALE cached kind within the TTL after the flag flips 'cuid'->'runOpsId'", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); let live: RunIdMintKind = "cuid"; const flag = makeCachedFlag(cache, () => live); @@ -39,12 +39,12 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window — current be expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // populates the cache - live = "ksuid"; // admin flips the org flag + live = "runOpsId"; // admin flips the org flag vi.advanceTimersByTime(TTL_MS - 1); // still inside the window expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // STALE, as designed }); - it("returns the FRESH kind once the TTL expires after a 'cuid'->'ksuid' flip", async () => { + it("returns the FRESH kind once the TTL expires after a 'cuid'->'runOpsId' flip", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); let live: RunIdMintKind = "cuid"; const flag = makeCachedFlag(cache, () => live); @@ -52,22 +52,22 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window — current be expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); - live = "ksuid"; + live = "runOpsId"; vi.advanceTimersByTime(TTL_MS + 1); // past expiry -> entry evicted on read - expect(await computeRunIdMintKind(env, deps)).toBe("ksuid"); // re-reads the live flag + expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); // re-reads the live flag }); - it("symmetric flip-back 'ksuid'->'cuid' is also stale within TTL, fresh after", async () => { + it("symmetric flip-back 'runOpsId'->'cuid' is also stale within TTL, fresh after", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); - let live: RunIdMintKind = "ksuid"; + let live: RunIdMintKind = "runOpsId"; const flag = makeCachedFlag(cache, () => live); const deps = { masterEnabled: true, splitEnabled: async () => true, flag }; - expect(await computeRunIdMintKind(env, deps)).toBe("ksuid"); + expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); live = "cuid"; vi.advanceTimersByTime(TTL_MS - 1); - expect(await computeRunIdMintKind(env, deps)).toBe("ksuid"); // STALE + expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); // STALE vi.advanceTimersByTime(2); // now past expiry expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // FRESH diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts index c539ee9d4a9..161e0683fd3 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts @@ -12,8 +12,8 @@ describe("computeRunIdMintKind (pure)", () => { expect(flag).not.toHaveBeenCalled(); }); - it("mints cuid when split is OFF, even if master + per-org flag say 'ksuid'", async () => { - const flag = vi.fn().mockResolvedValue("ksuid"); + it("mints cuid when split is OFF, even if master + per-org flag say 'runOpsId'", async () => { + const flag = vi.fn().mockResolvedValue("runOpsId"); const kind = await computeRunIdMintKind( { organizationId: "org_1", id: "env_1" }, { masterEnabled: true, splitEnabled: async () => false, flag } @@ -22,18 +22,18 @@ describe("computeRunIdMintKind (pure)", () => { expect(flag).not.toHaveBeenCalled(); // split-off short-circuits before any flag read }); - it("mints run-ops id only when master on AND split on AND per-org flag = 'ksuid'", async () => { - const flag = vi.fn().mockResolvedValue("ksuid"); + it("mints run-ops id only when master on AND split on AND per-org flag = 'runOpsId'", async () => { + const flag = vi.fn().mockResolvedValue("runOpsId"); const kind = await computeRunIdMintKind( { organizationId: "org_1", id: "env_1" }, { masterEnabled: true, splitEnabled: async () => true, flag } ); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); }); it("passes the already-loaded org feature flags through to the flag fn (no extra DB read)", async () => { - const flag = vi.fn().mockResolvedValue("ksuid"); - const orgFeatureFlags = { runOpsMintKsuid: "ksuid" }; + const flag = vi.fn().mockResolvedValue("runOpsId"); + const orgFeatureFlags = { runOpsMintKsuid: "runOpsId" }; await computeRunIdMintKind( { organizationId: "org_1", id: "env_1", orgFeatureFlags }, { masterEnabled: true, splitEnabled: async () => true, flag } diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts index c3751c993ce..97f8d622fbb 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts @@ -7,7 +7,7 @@ import { FEATURE_FLAG } from "~/v3/featureFlags"; import { makeFlag } from "~/v3/featureFlags.server"; import { isSplitEnabled } from "./splitMode.server"; -export type RunIdMintKind = "cuid" | "ksuid"; +export type RunIdMintKind = "cuid" | "runOpsId"; type MintKindDeps = { masterEnabled: boolean; @@ -54,7 +54,7 @@ export async function resolveRunIdMintKind(environment: { masterEnabled: env.RUN_OPS_MINT_KSUID_ENABLED, splitEnabled: isSplitEnabled, flag: async (orgId, orgFeatureFlags) => { - // The cache stores only "cuid"|"ksuid" (never undefined), so the cache's + // The cache stores only "cuid"|"runOpsId" (never undefined), so the cache's // "stored-undefined == miss" caveat never applies here. const cached = mintCache.get(orgId); if (cached !== undefined) return cached; diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index 53e03d17b08..bee0504a3de 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -361,7 +361,7 @@ export class BatchTriggerV3Service extends BaseService { orgFeatureFlags: environment.organization.featureFlags, }); - return mintKind === "ksuid" + return mintKind === "runOpsId" ? RunId.toFriendlyId(generateRunOpsId(region)) : RunId.generate().friendlyId; } diff --git a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts index c8492e7fa26..e57ddcb11b7 100644 --- a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts +++ b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts @@ -90,13 +90,13 @@ describe("BatchTriggerV3Service child-residency inheritance", () => { }); // The cuid mirror: a cuid batch anchor yields cuid children even if the flag flipped ON. - it("a cuid batch anchor yields cuid children even when the env flag resolves 'ksuid'", async () => { + it("a cuid batch anchor yields cuid children even when the env flag resolves 'runOpsId'", async () => { const service = new BatchTriggerV3Service( undefined, undefined, {} as any, {} as any, - async () => "ksuid" // env flag flipped ON mid-batch + async () => "runOpsId" // env flag flipped ON mid-batch ); const batchFriendlyId = BatchId.generate().friendlyId; // cuid (LEGACY) batch expect(ownerEngine(batchFriendlyId)).toBe("LEGACY"); diff --git a/apps/webapp/test/engine/triggerFailedTask.test.ts b/apps/webapp/test/engine/triggerFailedTask.test.ts index 2576e69863c..498312956aa 100644 --- a/apps/webapp/test/engine/triggerFailedTask.test.ts +++ b/apps/webapp/test/engine/triggerFailedTask.test.ts @@ -94,7 +94,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { await setupBackgroundWorker(engine, environment, taskIdentifier); const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); - expect(classifyKind(parentFriendlyId)).toBe("ksuid"); + expect(classifyKind(parentFriendlyId)).toBe("runOpsId"); await engine.trigger( { friendlyId: parentFriendlyId, @@ -122,7 +122,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { parentRunId: parentFriendlyId, }); - expect(classifyKind(friendlyId!)).toBe("ksuid"); + expect(classifyKind(friendlyId!)).toBe("runOpsId"); // The failed run write must land (persistence) and link to the resolved parent. const persisted = await prisma.taskRun.findFirst({ where: { friendlyId: friendlyId! } }); @@ -220,7 +220,7 @@ describe("TriggerFailedTaskService — failed run residency", () => { parentRunId: parentFriendlyId, }); - expect(classifyKind(friendlyId!)).toBe("ksuid"); + expect(classifyKind(friendlyId!)).toBe("runOpsId"); await engine.quit(); } diff --git a/apps/webapp/test/engine/triggerTask.test.ts b/apps/webapp/test/engine/triggerTask.test.ts index 2d235bcc1eb..4a3fc762600 100644 --- a/apps/webapp/test/engine/triggerTask.test.ts +++ b/apps/webapp/test/engine/triggerTask.test.ts @@ -2397,7 +2397,7 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = // lookup the child path performs. // v1 id (version "1" at index 25) → classifies NEW const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); - expect(classifyKind(parentFriendlyId)).toBe("ksuid"); + expect(classifyKind(parentFriendlyId)).toBe("runOpsId"); const parent = await triggerTaskService.call({ taskId: taskIdentifier, @@ -2413,7 +2413,7 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = body: { payload: { test: "child" }, options: { parentRunId: parentFriendlyId } }, }); - expect(classifyKind(child!.run.friendlyId)).toBe("ksuid"); + expect(classifyKind(child!.run.friendlyId)).toBe("runOpsId"); await engine.quit(); } @@ -2429,7 +2429,7 @@ describe("RunEngineTriggerTaskService — child run residency inheritance", () = const explicitFriendlyId = RunId.toFriendlyId(generateInternalId()); const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); expect(classifyKind(explicitFriendlyId)).toBe("cuid"); - expect(classifyKind(parentFriendlyId)).toBe("ksuid"); + expect(classifyKind(parentFriendlyId)).toBe("runOpsId"); const result = await triggerTaskService.call({ taskId: taskIdentifier, diff --git a/apps/webapp/test/runOpsMintCutover.test.ts b/apps/webapp/test/runOpsMintCutover.test.ts index 86c824862e9..844c26574ff 100644 --- a/apps/webapp/test/runOpsMintCutover.test.ts +++ b/apps/webapp/test/runOpsMintCutover.test.ts @@ -34,7 +34,7 @@ function mintRunOpsFriendlyId(): string { // Mirrors the real trigger path: resolve the kind, then mint either a run-ops friendlyId or // the default cuid one (RunId.generate()). function mintRunFriendlyId(kind: RunIdMintKind): string { - return kind === "ksuid" ? mintRunOpsFriendlyId() : RunId.generate().friendlyId; + return kind === "runOpsId" ? mintRunOpsFriendlyId() : RunId.generate().friendlyId; } async function seedOrgEnv(prisma: PrismaClient, mintFlag?: RunIdMintKind) { @@ -81,7 +81,7 @@ describe("per-env run-ops-id mint cutover", () => { postgresTest( "canary org mints run-ops/NEW; non-canary org mints cuid/LEGACY", async ({ prisma }) => { - const a = await seedOrgEnv(prisma, "ksuid"); // canary + const a = await seedOrgEnv(prisma, "runOpsId"); // canary const b = await seedOrgEnv(prisma); // not cut over const flag = realFlag(prisma); @@ -96,7 +96,7 @@ describe("per-env run-ops-id mint cutover", () => { deps ); - expect(kindA).toBe("ksuid"); + expect(kindA).toBe("runOpsId"); expect(kindB).toBe("cuid"); const friendlyA = mintRunFriendlyId(kindA); @@ -111,9 +111,9 @@ describe("per-env run-ops-id mint cutover", () => { ); postgresTest( - "split OFF mints cuid even for a 'ksuid'-flagged org (split gate dominates)", + "split OFF mints cuid even for a 'runOpsId'-flagged org (split gate dominates)", async ({ prisma }) => { - const a = await seedOrgEnv(prisma, "ksuid"); + const a = await seedOrgEnv(prisma, "runOpsId"); const flag = vi.fn(realFlag(prisma)); const kind = await computeRunIdMintKind( @@ -129,7 +129,7 @@ describe("per-env run-ops-id mint cutover", () => { postgresTest( "drain-new-forward (D8): flipping back to cuid stops new run-ops mints without reverting existing", async ({ prisma }) => { - const a = await seedOrgEnv(prisma, "ksuid"); + const a = await seedOrgEnv(prisma, "runOpsId"); const flag = realFlag(prisma); const deps = { masterEnabled: true, splitEnabled: async () => true, flag }; @@ -139,7 +139,7 @@ describe("per-env run-ops-id mint cutover", () => { deps ); const firstFriendly = mintRunFriendlyId(firstKind); - expect(firstKind).toBe("ksuid"); + expect(firstKind).toBe("runOpsId"); expect(ownerEngine(RunId.fromFriendlyId(firstFriendly))).toBe("NEW"); // Roll the org back to cuid (drain-new-forward — set the flag to "cuid"). @@ -168,7 +168,7 @@ describe("per-env run-ops-id mint cutover", () => { "parent and child re-resolve independently from their own org flag", async ({ prisma }) => { // Parent lives in a cut-over org; child is triggered into a NON-cut-over org. - const parentOrg = await seedOrgEnv(prisma, "ksuid"); + const parentOrg = await seedOrgEnv(prisma, "runOpsId"); const childOrg = await seedOrgEnv(prisma); // not cut over const flag = realFlag(prisma); const deps = { masterEnabled: true, splitEnabled: async () => true, flag }; @@ -186,7 +186,7 @@ describe("per-env run-ops-id mint cutover", () => { // it does NOT inherit the parent's residency. A child in a non-cut-over org mints cuid // even when its parent was born run-ops. If children must inherit, that inheritance // belongs to the child-trigger path, not this resolver. - expect(parentKind).toBe("ksuid"); + expect(parentKind).toBe("runOpsId"); expect(childKind).toBe("cuid"); } ); diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts index 2a042494f51..0c76fdc91ab 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts @@ -23,7 +23,7 @@ describe("ownerEngine — residency classifier (version char at fixed position, expect(ownerEngine(id)).toBe("NEW"); expect(ownerEngine(friendlyId)).toBe("NEW"); expect(classifyResidency(id)).toBe("NEW"); - expect(classifyKind(id)).toBe("ksuid"); + expect(classifyKind(id)).toBe("runOpsId"); } }); diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.ts b/packages/core/src/v3/isomorphic/runOpsResidency.ts index 2e960548174..404c7f40411 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.ts @@ -4,12 +4,12 @@ import { isRunOpsIdBody } from "./friendlyId.js"; export type Residency = "LEGACY" | "NEW"; /** - * Underlying id lineage. "ksuid" is the historical label for the NEW-store - * mint path (now a base32hex run-ops v1 id — see friendlyId.ts); it is kept - * because the value is persisted in the runOpsMintKsuid feature flag. "cuid" - * is every legacy shape (cuid, nanoid, pre-cutover 27-char base62). + * Underlying id lineage. "runOpsId" (formerly "ksuid") is the label for the + * NEW-store mint path — a base32hex run-ops v1 id (see friendlyId.ts). It is + * the value persisted in the runOpsMintKsuid feature flag. "cuid" is every + * legacy shape (cuid, nanoid, pre-cutover 27-char base62). */ -export type ResidencyKind = "cuid" | "ksuid"; +export type ResidencyKind = "cuid" | "runOpsId"; /** @bugsnag/cuid emits 25-char ids (legacy mint path, flag OFF). */ export const CUID_LENGTH = 25; @@ -43,14 +43,14 @@ function internalForm(id: string): string { /** * Returns the id lineage by the version-char rule: a well-formed run-ops v1 - * body (26 chars, version "1" at index 25, base32hex alphabet) is "ksuid" + * body (26 chars, version "1" at index 25, base32hex alphabet) is "runOpsId" * (NEW store); everything else — including malformed v1 shapes — is "cuid" * (legacy). Total: never throws. Transition: pre-cutover 27-char base62 KSUIDs (the old * NEW-mint format) now classify LEGACY, so ship this with the base32hex generator only once * any 27-char NEW-resident runs are drained/disposable — no live run is misrouted mid-cutover. */ export function classifyKind(id: string): ResidencyKind { - return isRunOpsIdBody(internalForm(id)) ? "ksuid" : "cuid"; + return isRunOpsIdBody(internalForm(id)) ? "runOpsId" : "cuid"; } /** Classification is total now; kept for API compatibility. */ @@ -60,7 +60,7 @@ export function isClassifiable(_id: string): boolean { /** Map an id to its owning run-ops store by the version-char rule. */ export function classifyResidency(id: string): Residency { - return classifyKind(id) === "ksuid" ? "NEW" : "LEGACY"; + return classifyKind(id) === "runOpsId" ? "NEW" : "LEGACY"; } /** Primary public name (RoutingRunStore / cross-seam guard). */ From f398bff8fae507737271be367c3688921d541b2c Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 01:22:47 +0100 Subject: [PATCH 6/9] chore(run-ops): rename env var RUN_OPS_MINT_ENABLED and flag key runOpsMintKind Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/webapp/app/env.server.ts | 4 ++-- apps/webapp/app/v3/featureFlags.ts | 6 +++--- .../app/v3/runOpsMigration/runOpsMintKind.server.test.ts | 2 +- apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts | 4 ++-- apps/webapp/test/runOpsMintCutover.test.ts | 6 +++--- packages/core/src/v3/isomorphic/runOpsResidency.ts | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index af43515d179..f5e0f0a671c 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -1728,9 +1728,9 @@ const EnvironmentSchema = z RUN_REPLICATION_NEW_ORIGIN_GENERATION: z.coerce.number().int().default(1), // Run-ops id mint cutover — per-env, canary-first, OFF by default. - // Even when on, an env mints run-ops ids only if its per-org runOpsMintKsuid flag is + // Even when on, an env mints run-ops ids only if its per-org runOpsMintKind flag is // "runOpsId" AND isSplitEnabled() is true. Cache mirrors REALTIME_BACKEND_FLAG_CACHE_*. - RUN_OPS_MINT_KSUID_ENABLED: BoolEnv.default(false), + RUN_OPS_MINT_ENABLED: BoolEnv.default(false), RUN_OPS_MINT_FLAG_CACHE_TTL_MS: z.coerce.number().int().default(30_000), RUN_OPS_MINT_FLAG_CACHE_MAX_ENTRIES: z.coerce.number().int().default(10_000), diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index cdf29906354..637830aef06 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -18,7 +18,7 @@ export const FEATURE_FLAG = { computeMigrationPaidPercentage: "computeMigrationPaidPercentage", computeMigrationRequireTemplate: "computeMigrationRequireTemplate", devBranchesEnabled: "devBranchesEnabled", - runOpsMintKsuid: "runOpsMintKsuid", + runOpsMintKind: "runOpsMintKind", } as const; export const FeatureFlagCatalog = { @@ -52,8 +52,8 @@ export const FeatureFlagCatalog = { // Per-org access to development branches. Off unless enabled for the org. [FEATURE_FLAG.devBranchesEnabled]: z.coerce.boolean(), // Per-org run-ops-id mint cutover. Defaults to "cuid"; only honored when - // RUN_OPS_MINT_KSUID_ENABLED is on AND isSplitEnabled() is true. - [FEATURE_FLAG.runOpsMintKsuid]: z.enum(["cuid", "runOpsId"]), + // RUN_OPS_MINT_ENABLED is on AND isSplitEnabled() is true. + [FEATURE_FLAG.runOpsMintKind]: z.enum(["cuid", "runOpsId"]), }; export type FeatureFlagKey = keyof typeof FeatureFlagCatalog; diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts index 161e0683fd3..bdc8692f3bd 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts @@ -33,7 +33,7 @@ describe("computeRunIdMintKind (pure)", () => { it("passes the already-loaded org feature flags through to the flag fn (no extra DB read)", async () => { const flag = vi.fn().mockResolvedValue("runOpsId"); - const orgFeatureFlags = { runOpsMintKsuid: "runOpsId" }; + const orgFeatureFlags = { runOpsMintKind: "runOpsId" }; await computeRunIdMintKind( { organizationId: "org_1", id: "env_1", orgFeatureFlags }, { masterEnabled: true, splitEnabled: async () => true, flag } diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts index 97f8d622fbb..305b851bdcb 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts @@ -51,7 +51,7 @@ export async function resolveRunIdMintKind(environment: { orgFeatureFlags?: unknown; }): Promise { return computeRunIdMintKind(environment, { - masterEnabled: env.RUN_OPS_MINT_KSUID_ENABLED, + masterEnabled: env.RUN_OPS_MINT_ENABLED, splitEnabled: isSplitEnabled, flag: async (orgId, orgFeatureFlags) => { // The cache stores only "cuid"|"runOpsId" (never undefined), so the cache's @@ -73,7 +73,7 @@ export async function resolveRunIdMintKind(environment: { )?.featureFlags; const kind = await flagFn({ - key: FEATURE_FLAG.runOpsMintKsuid, + key: FEATURE_FLAG.runOpsMintKind, defaultValue: "cuid", overrides: (overrides as Record) ?? {}, }); diff --git a/apps/webapp/test/runOpsMintCutover.test.ts b/apps/webapp/test/runOpsMintCutover.test.ts index 844c26574ff..0fd9e40fffb 100644 --- a/apps/webapp/test/runOpsMintCutover.test.ts +++ b/apps/webapp/test/runOpsMintCutover.test.ts @@ -48,7 +48,7 @@ async function seedOrgEnv(prisma: PrismaClient, mintFlag?: RunIdMintKind) { if (mintFlag) { await prisma.organization.update({ where: { id: organization.id }, - data: { featureFlags: { [FEATURE_FLAG.runOpsMintKsuid]: mintFlag } }, + data: { featureFlags: { [FEATURE_FLAG.runOpsMintKind]: mintFlag } }, }); } return { organization, environment }; @@ -70,7 +70,7 @@ function realFlag(prisma: PrismaClient) { }) )?.featureFlags; return flagFn({ - key: FEATURE_FLAG.runOpsMintKsuid, + key: FEATURE_FLAG.runOpsMintKind, defaultValue: "cuid", overrides: (overrides as Record) ?? {}, }); @@ -145,7 +145,7 @@ describe("per-env run-ops-id mint cutover", () => { // Roll the org back to cuid (drain-new-forward — set the flag to "cuid"). await prisma.organization.update({ where: { id: a.organization.id }, - data: { featureFlags: { [FEATURE_FLAG.runOpsMintKsuid]: "cuid" } }, + data: { featureFlags: { [FEATURE_FLAG.runOpsMintKind]: "cuid" } }, }); // The NEXT run mints cuid again (the env-bound resolver's TTL cache is not used here, diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.ts b/packages/core/src/v3/isomorphic/runOpsResidency.ts index 404c7f40411..29bfcc28867 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.ts @@ -6,7 +6,7 @@ export type Residency = "LEGACY" | "NEW"; /** * Underlying id lineage. "runOpsId" (formerly "ksuid") is the label for the * NEW-store mint path — a base32hex run-ops v1 id (see friendlyId.ts). It is - * the value persisted in the runOpsMintKsuid feature flag. "cuid" is every + * the value persisted in the runOpsMintKind feature flag. "cuid" is every * legacy shape (cuid, nanoid, pre-cutover 27-char base62). */ export type ResidencyKind = "cuid" | "runOpsId"; From 9cfda5af843663ad0789d4b166630a4e00c3816b Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 01:26:47 +0100 Subject: [PATCH 7/9] chore(run-ops): purge ksuid from comments + changeset (migration file left for checksum) Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/olive-bees-repeat.md | 2 +- .../core/src/v3/isomorphic/runOpsResidency.test.ts | 2 +- packages/core/src/v3/isomorphic/runOpsResidency.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/olive-bees-repeat.md b/.changeset/olive-bees-repeat.md index 3702f284f51..54ac6b0352f 100644 --- a/.changeset/olive-bees-repeat.md +++ b/.changeset/olive-bees-repeat.md @@ -2,4 +2,4 @@ "@trigger.dev/core": patch --- -Change the run-ops run id format from a 27-char base62 KSUID to a 26-char lowercase base32hex id (ms timestamp + CSPRNG core, region char, version char) so run ids are DNS-1123 safe for Kubernetes pod names while staying lexicographically time-sortable, and rename the exported `ResidencyKind` value `"ksuid"` to `"runOpsId"` to reflect that the NEW-store id is no longer a KSUID +Change the run-ops run id format from a 27-char base62 id to a 26-char lowercase base32hex id (ms timestamp + CSPRNG core, region char, version char) so run ids are DNS-1123 safe for Kubernetes pod names while staying lexicographically time-sortable, and rename the exported `ResidencyKind` value to `"runOpsId"` (was the pre-cutover base62-format label) to reflect the new format diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts index 0c76fdc91ab..ceb6d9b5358 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts @@ -41,7 +41,7 @@ describe("ownerEngine — residency classifier (version char at fixed position, "A".repeat(25) + "1", // uppercase "a".repeat(24) + "-1", // hyphen region char "a".repeat(27), // pre-cutover 27-char shape → LEGACY under the version rule - "run_" + "b".repeat(27), // 27-char base62 KSUID-era friendly id → LEGACY + "run_" + "b".repeat(27), // 27-char base62 pre-cutover friendly id → LEGACY "x".repeat(40), ]) { expect(ownerEngine(bad)).toBe("LEGACY"); diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.ts b/packages/core/src/v3/isomorphic/runOpsResidency.ts index 29bfcc28867..f0b1e3db334 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.ts @@ -4,10 +4,10 @@ import { isRunOpsIdBody } from "./friendlyId.js"; export type Residency = "LEGACY" | "NEW"; /** - * Underlying id lineage. "runOpsId" (formerly "ksuid") is the label for the - * NEW-store mint path — a base32hex run-ops v1 id (see friendlyId.ts). It is - * the value persisted in the runOpsMintKind feature flag. "cuid" is every - * legacy shape (cuid, nanoid, pre-cutover 27-char base62). + * Underlying id lineage. "runOpsId" is the label for the NEW-store mint path + * — a base32hex run-ops v1 id (see friendlyId.ts). It is the value persisted in + * the runOpsMintKind feature flag. "cuid" is every legacy shape (cuid, nanoid, + * and the pre-cutover 27-char base62 format). */ export type ResidencyKind = "cuid" | "runOpsId"; @@ -45,7 +45,7 @@ function internalForm(id: string): string { * Returns the id lineage by the version-char rule: a well-formed run-ops v1 * body (26 chars, version "1" at index 25, base32hex alphabet) is "runOpsId" * (NEW store); everything else — including malformed v1 shapes — is "cuid" - * (legacy). Total: never throws. Transition: pre-cutover 27-char base62 KSUIDs (the old + * (legacy). Total: never throws. Transition: pre-cutover 27-char base62 ids (the old * NEW-mint format) now classify LEGACY, so ship this with the base32hex generator only once * any 27-char NEW-resident runs are drained/disposable — no live run is misrouted mid-cutover. */ From ba796ecfdbefbf60623c555b60c7f4a1131bf164 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 01:28:48 +0100 Subject: [PATCH 8/9] fix(ci): retry prisma generate to survive Windows EPERM engine-DLL race `@trigger.dev/database` and `@internal/run-ops-database` pin the same prisma version, so pnpm gives them one shared package instance in the store. When `turbo run generate` runs their generate scripts concurrently, both prisma processes race to write the query-engine binary into that shared directory and on Windows the loser fails with `EPERM: operation not permitted, rename` on the `.tmp -> .dll.node` step, killing CI before any test runs. Wrap `prisma generate` in a small cross-platform node script that retries on transient filesystem contention (EPERM/EBUSY/EACCES) with backoff. On non-Windows the first attempt always succeeds, so this is a zero-cost no-op. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e.yml | 2 + internal-packages/database/package.json | 2 +- .../run-ops-database/package.json | 2 +- scripts/retry-prisma-generate.mjs | 53 +++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 scripts/retry-prisma-generate.mjs diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d8aa9ec7d7d..2ec5ac470bc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -42,6 +42,8 @@ jobs: - name: 📥 Download deps run: pnpm install --frozen-lockfile --filter trigger.dev... + # Prisma clients generate concurrently and share one engine binary in the + # pnpm store; the generate script retries the transient Windows EPERM race. - name: 📀 Generate Prisma Client run: pnpm run generate diff --git a/internal-packages/database/package.json b/internal-packages/database/package.json index ec0bc950b8c..560cf229dd0 100644 --- a/internal-packages/database/package.json +++ b/internal-packages/database/package.json @@ -15,7 +15,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "prisma migrate deploy", "db:push": "prisma db push", diff --git a/internal-packages/run-ops-database/package.json b/internal-packages/run-ops-database/package.json index cbeb0279b05..7bfe0fe271c 100644 --- a/internal-packages/run-ops-database/package.json +++ b/internal-packages/run-ops-database/package.json @@ -25,7 +25,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "node scripts/migrate.mjs deploy", "db:migrate:status": "node scripts/migrate.mjs status", diff --git a/scripts/retry-prisma-generate.mjs b/scripts/retry-prisma-generate.mjs new file mode 100644 index 00000000000..f20fa4616ca --- /dev/null +++ b/scripts/retry-prisma-generate.mjs @@ -0,0 +1,53 @@ +// Retry wrapper around `prisma generate`. Our two prisma clients pin the same +// prisma version and so share one package instance in the pnpm store; when +// `turbo run generate` runs them concurrently both race to write the shared +// query-engine binary, and on Windows the loser fails with `EPERM ... rename`. +// Retrying lets it succeed once the engine file is present and unlocked. On +// non-Windows the first attempt succeeds, so this is a zero-cost no-op. +import { spawnSync } from "node:child_process"; + +const MAX_ATTEMPTS = 5; +const BASE_DELAY_MS = 500; + +// Transient, retryable filesystem contention on the shared engine binary. +const TRANSIENT = + /\b(EPERM|EBUSY|EACCES)\b|operation not permitted|resource busy or locked|being used by another process/i; + +const passthroughArgs = process.argv.slice(2); + +function sleepSync(ms) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); +} + +let lastStatus = 1; + +for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const result = spawnSync("prisma", ["generate", ...passthroughArgs], { + shell: true, + encoding: "utf8", + }); + + process.stdout.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + + if (result.status === 0) { + process.exit(0); + } + + lastStatus = result.status ?? 1; + + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; + const isRetryable = TRANSIENT.test(output); + + if (!isRetryable || attempt === MAX_ATTEMPTS) { + break; + } + + const delay = BASE_DELAY_MS * attempt; + console.error( + `prisma generate hit a transient filesystem error (attempt ${attempt}/${MAX_ATTEMPTS}); retrying in ${delay}ms...` + ); + sleepSync(delay); +} + +process.exit(lastStatus); From 305ed4a57edc785e0b3b4a2c86864cdc05c48183 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 09:40:56 +0100 Subject: [PATCH 9/9] chore(run-ops): remove changeset from run-id PR Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/olive-bees-repeat.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/olive-bees-repeat.md diff --git a/.changeset/olive-bees-repeat.md b/.changeset/olive-bees-repeat.md deleted file mode 100644 index 54ac6b0352f..00000000000 --- a/.changeset/olive-bees-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@trigger.dev/core": patch ---- - -Change the run-ops run id format from a 27-char base62 id to a 26-char lowercase base32hex id (ms timestamp + CSPRNG core, region char, version char) so run ids are DNS-1123 safe for Kubernetes pod names while staying lexicographically time-sortable, and rename the exported `ResidencyKind` value to `"runOpsId"` (was the pre-cutover base62-format label) to reflect the new format