Skip to content

Add admin pet generation pilot - #41

Open
astandrik wants to merge 2 commits into
mainfrom
codex/admin-pet-generation-pilot
Open

Add admin pet generation pilot#41
astandrik wants to merge 2 commits into
mainfrom
codex/admin-pet-generation-pilot

Conversation

@astandrik

@astandrik astandrik commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • add the admin-only generation run state machine, YDB persistence, private artifacts, and admin API/UI
  • add a concurrency-one worker with leased stages, bounded provider retries, a call ledger, moderation, and deterministic Codex Pets v2 assembly
  • link final approval to existing pending-pet moderation and document the disabled-by-default rollout and rollback

Verification

  • npm test (970 passed, 2 skipped)
  • npm run lint -- --quiet
  • npx tsc --noEmit --incremental false
  • npm run build
  • PET_GENERATION_ENABLED=false npm run generation:worker
  • headed admin run-card QA at desktop and 390 px

Rollout boundary

This PR does not apply the YDB migration, deploy the worker, mount an OpenAI key, enable the feature, or run the five-request pilot.

Greptile Summary

This change adds an administrative pet-generation workflow with provider execution, generated artifacts, moderation, and generation-request tracking. A pending pet can become available for approval before its generation run is durably linked to it; approval in that window publishes the pet but leaves the run and its request incomplete.

Confidence Score: 4/5

Not safe to merge until pending-pet creation and generation-run linkage cannot be separated, or the workflow can recover the linkage before moderation approval.

A focused executable test exercised the normal linked approval path and the unlinked interruption path. The normal path completed the generation run and request, while the interrupted path left both records incomplete after the pet was approved.

Files Needing Attention: src/lib/pets/generation/submission.ts needs atomicity or recovery for pending-pet linkage. src/lib/pets/generation/repository.ts and src/app/api/admin/submissions/[id]/approve/route.ts need to ensure approval cannot silently bypass completion of an unlinked generation run.

T-Rex T-Rex Logs

What T-Rex did

  • Ran a focused executable harness to exercise linked and unlinked generation moderation states and post a P1 finding.
  • Verified that the focused validation harness typechecks cleanly before running validation tests.
  • Validated the posted finding through the contract-validation workflow and observed that the linked generation completed while the unlinked run remained in progress.
  • Executed the described command set, including test runs and type checks, to reproduce and sanity-check the generation-linking logic.
  • From the validation results, the linked path completed while the unlinked path remained in submitting, indicating a behavior mismatch in the moderation flow.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Approving a pending generated pet can strand its generation run and request before final-pet linkage

    • Bug
      • submitGenerationRun creates a moderation-visible pending pet, then performs a separate conditional run transition that writes finalPetId/finalPetSlug. If that second operation fails or the process stops, the pet remains approvable. The approve route approves it and calls completeGeneratedPetModeration, but that helper cannot find a run by the pet ID and returns immediately. Executed evidence confirms the run remains submitting and unlinked and the request remains in_progress and unlinked.
    • Cause
      • Creation of the pet and association/transition of the generation run are not atomic and have no recovery/compensation path. Moderation completion only handles runs already linked by finalPetId and in awaiting_moderation.
    • Fix
      • Make persistence of the pending pet and generation run’s final-pet linkage atomic, or prevent moderation visibility until linkage succeeds. Add recovery for an existing deterministic pending pet that locates its submitting run and completes the linkage; moderation should not silently return when approving a generated-but-unlinked pet.

    T-Rex Ran code and verified through T-Rex

Fix All in Codex

Prompt To Fix All With AI
### Issue 1
src/lib/pets/generation/submission.ts:97-104
**Pending pet can be approved before generation linkage**

`createPendingPet` persists a moderation-visible pet before the separate transition records `finalPetId` and moves the run to `awaiting_moderation`. If execution stops or that transition fails, an administrator can approve the persisted pet, but `completeGeneratedPetModeration` cannot find an `awaiting_moderation` run by its ID and returns without completing the run or fulfilling its request. Make pet creation and run linkage atomic, or keep the pet unavailable for moderation until linkage succeeds and recover already-created pending pets.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Add admin pet generation pilot" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Add the admin-only generation state machine, private YDB artifact storage, leased worker, deterministic v2 package assembly, review UI, moderation linkage, migration, tests, and rollout documentation. Generation remains disabled by default and OpenAI credentials stay worker-only.
Copilot AI lite review requested due to automatic review settings August 9, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an admin-only “pet generation” pilot across the web app and a separate worker process, backed by new YDB tables and a run state machine, with artifacts persisted privately and final approval tied into the existing moderation flow.

Changes:

  • Add YDB schema + migration for generation runs, stage attempts, and chunked artifacts.
  • Implement generation-run persistence/state transitions, submission→pending-pet creation (deterministic IDs), and moderation linkage.
  • Add admin API routes and UI components for creating/controlling runs and viewing moderated artifacts; add a single-concurrency worker script + OpenAI provider pipeline.

Reviewed changes

Copilot reviewed 58 out of 58 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ydb/schema.yql Adds generation run/attempt/artifact tables to the canonical schema.
ydb/migrations/20260808_001_add_pet_generation_runs.mjs Adds migration to create new generation tables when missing.
src/lib/ydb/schema.ts Registers new generation table names in app constants.
src/lib/pets/repository.ts Adds deterministic pending-pet IDs for replay-safe generation submissions.
src/lib/pets/mock-data.ts Allows deterministic mock pet IDs to support replay/idempotency tests.
src/lib/pets/generation/types.ts Defines generation run/stage/attempt/artifact types and enums.
src/lib/pets/generation/submission.ts Implements final approval submission: validate artifacts/package and create deterministic pending pet.
src/lib/pets/generation/submission.test.ts Tests replay-safe submission and moderation linkage using mock datasource.
src/lib/pets/generation/state-machine.ts Adds run status transition rules and conflict error type.
src/lib/pets/generation/retry-policy.ts TS re-export wrapper for runtime retry policy.
src/lib/pets/generation/retry-policy-runtime.mjs Implements bounded retry decisions and call budget enforcement.
src/lib/pets/generation/retry-policy-runtime.d.mts Adds types for retry policy runtime module.
src/lib/pets/generation/repository.ts Adds YDB-backed generation run + artifact persistence and admin mutations.
src/lib/pets/generation/repository-idempotency.test.ts Tests run creation idempotency and conflict behavior (mock).
src/lib/pets/generation/input.ts TS re-export wrapper for reference-image normalization runtime.
src/lib/pets/generation/input-runtime.mjs Validates/normalizes reference images into safe PNG inputs.
src/lib/pets/generation/input-runtime.d.mts Types for normalized reference image helper.
src/lib/pets/generation/final-metadata.ts Validates final moderator-owned metadata (id/name/desc/kind/tags).
src/lib/pets/generation/contracts.test.ts Contract tests for config defaults, transitions, chunking, retry policy, and metadata/ref normalization.
src/lib/pets/generation/config.ts TS re-export wrapper for generation config runtime.
src/lib/pets/generation/config-runtime.mjs Defines env-driven, bounded generation config with disabled-by-default defaults.
src/lib/pets/generation/config-runtime.d.mts Types for generation config runtime module.
src/lib/pets/generation/artifact-route.test.ts Tests admin-only artifact route behavior and header hardening.
src/lib/pets/generation/artifact-chunks.ts Implements chunking + SHA verification for stored artifacts.
src/lib/pets/generation/admin-api.ts Adds admin gate + idempotency key parsing + mutation response helper.
src/lib/pets/generation/admin-api.test.ts Tests admin gate ordering, disabled-flag behavior, and idempotency key validation.
src/lib/pets/generation-requests-repository.ts Exposes getGenerationRequestById and adds request “reopen” helper.
src/components/GenerationRequestsTable/GenerationRequestsTable.tsx Displays latest run card per request and disables manual actions while automation active.
src/components/AdminGenerationRunCard/AdminGenerationRunCard.tsx Adds admin UI for run progress, artifact previews, and run actions/approval form.
src/components/AdminGenerationRunCard/AdminGenerationRunCard.scss Styles the new run card and previews.
src/components/AdminGenerationRequestActions/AdminGenerationRequestActions.tsx Adds automationActive mode to route admins to run actions.
src/app/api/admin/submissions/[id]/reject/route.ts Links rejection to generated-run reopening logic.
src/app/api/admin/submissions/[id]/delete/route.ts Links deletion to generated-run reopening logic.
src/app/api/admin/submissions/[id]/approve/route.ts Links approval to generation run completion.
src/app/api/admin/generation-runs/[id]/retry/route.ts Adds admin endpoint to retry failed runs.
src/app/api/admin/generation-runs/[id]/regenerate-base/route.ts Adds admin endpoint to regenerate the base image.
src/app/api/admin/generation-runs/[id]/cancel/route.ts Adds admin endpoint to cancel a run.
src/app/api/admin/generation-runs/[id]/artifacts/[key]/route.ts Adds private admin artifact download/preview endpoint.
src/app/api/admin/generation-runs/[id]/approve-final/route.ts Adds admin endpoint to submit final metadata and create pending pet.
src/app/api/admin/generation-runs/[id]/approve-base/route.ts Adds admin endpoint to approve the base image and queue hatch.
src/app/api/admin/generation-requests/[id]/runs/route.ts Adds admin endpoint to create a run with Idempotency-Key support.
src/app/admin/requests/page.tsx Loads latest runs and passes generation-enabled flag into admin requests UI.
scripts/lib/pet-generation-worker-ydb.mjs Adds YDB client bootstrap for the standalone worker container.
scripts/lib/pet-generation-worker-runtime.test.ts Tests worker orchestration, leasing, retries, moderation, and budgeting with an in-memory repository.
scripts/lib/pet-generation-worker-runtime.mjs Implements concurrency-one worker runtime with call ledger, retries, and artifact writing.
scripts/lib/pet-generation-worker-runtime.d.mts Declares worker runtime types for TS consumers/tests.
scripts/lib/pet-generation-worker-repository.mjs Implements YDB-backed worker repository: leasing, attempts ledger, artifact chunk persistence, cleanup.
scripts/lib/pet-generation-provider.test.ts Tests OpenAI provider request shapes and ambiguous-loss behavior.
scripts/lib/pet-generation-provider.mjs Implements OpenAI image/moderation/review provider with strict validation and sizing limits.
scripts/lib/pet-generation-provider.d.mts Type declarations for the OpenAI provider module.
scripts/lib/pet-generation-pipeline.test.ts Tests deterministic v2 pipeline assembly and artifact outputs.
scripts/lib/pet-generation-pipeline.mjs Implements deterministic v2 sprite pipeline (grid processing, atlas assembly, QA artifacts).
scripts/lib/pet-generation-pipeline.d.mts Type declarations for pipeline module.
scripts/generation-worker.mjs Adds runnable worker entrypoint wiring config, YDB repo, provider, and polling loop.
package.json Adds generation:worker script entry.
DEPLOYMENT.md Documents rollout boundary, worker container setup, and additive rollout/rollback steps.
deploy/generation-worker.env.runtime.example Adds example env file for the worker container (includes key mount).
deploy/app-session.env.runtime.example Adds web-container env notes for generation pilot (explicitly no OpenAI key).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +16 to +21
return new Response(new Uint8Array(artifact.buffer), { headers: {
"Cache-Control": "private, no-store",
"Content-Type": artifact.metadata.contentType,
"Content-Disposition": `inline; filename="${safeName(artifact.metadata.fileName)}"`,
"X-Content-Type-Options": "nosniff",
} });
Comment on lines +101 to +114
export async function listLatestGenerationRunsByRequestIds(ids: readonly string[]) {
const wanted = new Set(ids);
if (!wanted.size) return new Map<string, PetGenerationRun>();
let runs: PetGenerationRun[];
if (isMockPetsDataSource()) runs = Array.from(mockRuns.values());
else if (!isYdbConfigured()) runs = [];
else runs = rowsFromResult(await withSession((session) =>
session.executeQuery(`SELECT ${RUN_COLUMNS} FROM ${TABLES.generationRuns} ORDER BY created_at DESC LIMIT 500;`),
)).map(parseRun);
const result = new Map<string, PetGenerationRun>();
for (const run of runs.filter((item) => wanted.has(item.requestId)).sort((a, b) => b.createdAt.localeCompare(a.createdAt))) {
if (!result.has(run.requestId)) result.set(run.requestId, run);
}
return result;
Comment on lines +139 to +141
<div className="admin-generation-run-card__progress" aria-label={`${progress}% complete`}>
<span style={{ width: `${progress}%` }} />
</div>
Comment on lines +554 to +557
if (input.deterministicPetId) {
const existing = await getPetById(input.deterministicPetId);
if (existing) return toPublicPet(existing, EMPTY_METRICS, await getOwnerProfileByRow(existing));
}
Comment on lines +97 to +104
const transitioned = await transitionGenerationRun({
runId: run.id,
status: "awaiting_moderation",
finalMetadata: input.metadata,
finalPetId: pet.id,
finalPetSlug: pet.slug,
approvedBy: input.approvedBy,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Pending pet can be approved before generation linkage

createPendingPet persists a moderation-visible pet before the separate transition records finalPetId and moves the run to awaiting_moderation. If execution stops or that transition fails, an administrator can approve the persisted pet, but completeGeneratedPetModeration cannot find an awaiting_moderation run by its ID and returns without completing the run or fulfilling its request. Make pet creation and run linkage atomic, or keep the pet unavailable for moderation until linkage succeeds and recover already-created pending pets.

Artifacts

Focused executable harness for linked and unlinked generation moderation states

  • A Vitest harness was authored and run against mock persistence to compare moderation after a linked versus unlinked pending-pet creation, showing the unlinked approval leaves both records incomplete.

Control run log for an already linked awaiting-moderation generation

  • The focused control command passed after approving a pet linked to an awaiting-moderation run, showing the expected completed run and fulfilled request path.

Race reproduction log for an unlinked pet while the run is submitting

  • The focused race command passed its assertions after approving a pet created while its run remained submitting, showing the incomplete-state consequence is reproducible.

Combined focused generation linkage test log

  • The full focused harness passed both the linked control and unlinked race assertions in one execution, confirming the behavioral contrast.

Typecheck log for the focused validation harness

  • The repository TypeScript check completed successfully after adding only the artifact-contained validation harness, confirming it compiles cleanly.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/pets/generation/submission.ts
Line: 97-104

Comment:
**Pending pet can be approved before generation linkage**

`createPendingPet` persists a moderation-visible pet before the separate transition records `finalPetId` and moves the run to `awaiting_moderation`. If execution stops or that transition fails, an administrator can approve the persisted pet, but `completeGeneratedPetModeration` cannot find an `awaiting_moderation` run by its ID and returns without completing the run or fulfilling its request. Make pet creation and run linkage atomic, or keep the pet unavailable for moderation until linkage succeeds and recover already-created pending pets.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d770f43da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +175 to +179
automationActive={Boolean(
row.generationRun &&
!["cancelled", "submission_rejected"].includes(
row.generationRun.status,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve manual actions when generation is disabled

When the pilot is rolled back with PET_GENERATION_ENABLED=false, a request with any existing run other than cancelled or submission_rejected still sets automationActive, so the table hides the manual attach/reject controls. The remaining run actions all pass through requireGenerationAdmin() and return 503 while disabled, leaving queued, failed, or review-stage requests with no usable admin workflow despite the rollback procedure in DEPLOYMENT.md stating that manual flows continue.

Useful? React with 👍 / 👎.

Comment on lines +31 to +35
const changed = await transitionGenerationRun({
runId: run.id,
status: "submitting",
lastStage: "submission",
finalMetadata: input.metadata,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recover runs when final submission fails

If any subsequent artifact read, package validation, asset write, or pet write fails after this transition, the run remains submitting. The worker does not process that status, and after a refresh the run card no longer renders the approval form, retry button, or cancel button, so the admin cannot recover the request through the UI; transition the run to a recoverable state on failure or keep a submission retry action available.

Useful? React with 👍 / 👎.

Comment on lines +243 to +245
if (run.targetedRetryCount > 0) return run.lastStage === stage
? { key: `work-${alias}-t${run.targetedRetryCount}`, alias }
: { key: alias, alias };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Regenerate look rows after retrying the cardinal source

When mechanical QA attributes a failure to cardinal, a targeted retry gives only that stage a revision key while look-row-9 and look-row-10 reuse their aliases. Those two images were generated with the old cardinal image as an explicit reference in hatchV2Pet, so the retry can combine a new cardinal guide with stale dependent direction rows and approve an inconsistent atlas; cardinal retries need to invalidate and regenerate both look rows as well.

Useful? React with 👍 / 👎.

Comment on lines +299 to +300
SELECT run_id,artifact_key FROM ${TABLES.artifacts}
WHERE retained=$retained AND expires_at<$expires_at LIMIT 100;`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Select cleanup-eligible artifacts before applying the limit

The query limits expired artifacts to 100 before the loop excludes nonterminal runs. Once 100 expired artifacts belong to failed, review-stage, or moderation-stage runs, each hourly pass can repeatedly select and skip that same set, preventing expired artifacts from terminal runs from ever being reached; this is realistic because a single hatch stores dozens of artifacts. Filter or paginate by terminal eligibility before imposing the batch limit so retention cleanup cannot starve.

Useful? React with 👍 / 👎.

Comment on lines +113 to +115
await requireRunUpdate(repository, claimed.run.id, {
status: "validating", lastStage: "assembly", expectedStatuses: ["generating"],
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resume validating runs through the expected state

If the worker dies after moving a hatch run to validating but before all artifacts are stored, findRunnableRun selects it and claimRun deliberately preserves validating; the resumed execution then reaches this update with expectedStatuses: ["generating"], treats the mismatch as cancellation, and leaves the run in validating. Every subsequent poll repeats the same path, so this crash window permanently loops instead of completing or failing the run.

Useful? React with 👍 / 👎.

Comment thread src/lib/pets/generation/repository.ts Outdated
Comment on lines +315 to +316
const confirmed = await getGenerationRunById(id);
return confirmed?.updatedAt === next.updatedAt ? { ok: true, run: confirmed } : conflict("Run changed concurrently.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Confirm the requested transition rather than only its timestamp

Under concurrent run actions, two transitions can compute the same millisecond-resolution updatedAt; one conditional update can lose because the status changed, yet this check still reports success when the winner stored that same timestamp, even if its status is different from the requested one. This can make an approve, cancel, or retry response claim success for an action that never occurred, so confirmation must also verify the intended status or use a monotonic version returned by the conditional write.

Useful? React with 👍 / 👎.

Comment on lines +42 to +47
const [request, spritesheet] = await Promise.all([
getGenerationRequestById(run.requestId),
readGenerationArtifact({ runId: run.id, key: "spritesheet" }),
]);
if (!request || !spritesheet) {
return { ok: false, error: "invalid_package", message: "Required final artifacts are missing." };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject submission after the source request is closed

If an admin rejects or deletes the generation request while its run is active, these existing mutation routes do not cancel the run and getGenerationRequestById still returns the closed record, so this check accepts it and creates a pending pet anyway. A rejected request can later be changed back to fulfilled when that pet is approved, while a deleted request leaves an approved pet with a run stuck in awaiting_moderation; require the request to remain pending or in_progress, and coordinate rejection/deletion with run cancellation.

AGENTS.md reference: src/AGENTS.md:L70-L70

Useful? React with 👍 / 👎.

Comment on lines +27 to +28
if (run.status !== "awaiting_final_review" && run.status !== "submitting") {
return { ok: false, error: "conflict", message: "Run is not awaiting final review." };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize replays while a submission is in progress

Once one approval has changed the run to submitting but is still assembling or storing the package, a second request is admitted here and may supply different metadata. Both requests can pass the deterministic-ID existence check before either pet write; if their metadata IDs produce different slugs, codex_pets accepts both because its primary key is the slug, leaving two rows with the same generated pet ID and a shared asset ID whose contents are overwritten by the last writer. Serialize this stage or make replays use the persisted final metadata under an idempotent transaction.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e815d64c9e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

enabled: env.PET_GENERATION_ENABLED?.trim().toLowerCase() === "true",
model: env.PET_GENERATION_MODEL?.trim() || PET_GENERATION_DEFAULT_MODEL,
reviewModel: env.PET_GENERATION_REVIEW_MODEL?.trim() || PET_GENERATION_DEFAULT_REVIEW_MODEL,
maxImageCalls: bounded(env.PET_GENERATION_MAX_IMAGE_CALLS, 15, 1, 15),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Raise the image budget for cardinal retries

With the default—and maximum permitted—budget of 15, a cardinal QA retry cannot complete: the initial base plus 12 hatch images consumes 13 calls, and retrying cardinal now correctly regenerates cardinal and both dependent look rows, requiring calls 14–16. The third regenerated image therefore receives image_call_budget_exhausted, while the run has already consumed its sole targeted retry, so this failure is unrecoverable without starting over.

Useful? React with 👍 / 👎.

if (!request) {
return NextResponse.json({ error: "not_found" }, { status: 404 });
}
if (guard.runId) await cancelGenerationRun(guard.runId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make request closure and run cancellation atomic

In the reject route, if the worker changes an awaiting_final_review run to submitting after the guard returns but before rejectGenerationRequest completes, this cancellation is rejected because submitting -> cancelled is not allowed, and its result is ignored; submission then observes the closed request and returns with the run permanently stuck in submitting. The fulfill and delete routes use the same ordering. Fresh evidence in this revision is that the newly added guard and cancellation remain separated by the request write and do not check cancellation failure, so the previously identified closure race is still possible.

AGENTS.md reference: src/AGENTS.md:L70-L70

Useful? React with 👍 / 👎.

Comment on lines +86 to +89
if (!input.allowExpired) {
const run = await readRun(execute, input.runId);
if (!run) return { kind: "missing" };
if (TERMINAL.has(run.status)) return { kind: "cancelled" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop stale workers after losing their lease

If both heartbeat updates fail long enough for the orchestration and provider leases to expire, a replacement worker marks the provider attempt ambiguous and moves the run to failed, but the original provider call can still return and continue because provider acquisition rejects only terminal statuses, not failed or a lost orchestration lease. Its token-guarded finishAttempt silently becomes a no-op, yet it can dispatch the remaining expensive image calls and overwrite aliases, potentially concurrently with an admin-triggered targeted retry; require the run to remain in its claimed generating state and verify lease ownership before permitting another provider attempt.

Useful? React with 👍 / 👎.

const pet = await createPendingPet({
deterministicPetId: deterministicPetId(run.id),
petJson: validation.value.petJson,
ownerId: request.requesterUserId ?? "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile owner deletion with the generation run

For a logged-in requester, assigning the generated pending pet to requesterUserId lets the existing owner-delete endpoint soft-delete it while the run is awaiting_moderation. Only the admin reject/delete routes call reopenGeneratedPetRequest; the owner route does not, so the run remains active, the request remains in_progress, and the deleted pet can no longer be moderated or recovered through the generation UI. Route owner deletion through the same generation reconciliation before exposing the pending pet to its owner.

AGENTS.md reference: src/AGENTS.md:L70-L70

Useful? React with 👍 / 👎.

Comment on lines +13 to +15
if (normalizeContentType(input.declaredContentType) !== detected) {
throw new Error("Reference image content type does not match its bytes.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate reference bytes when accepting the request

A multipart request can declare image/png while supplying a readable JPEG (or another allowed format), because the public request route checks the declared MIME and calls only sharp(...).metadata(). The request is accepted and shown as generation-ready, but this later byte/MIME equality check deterministically fails before any generation call; retrying the failed base repeats the same failure and consumes the run's sole targeted retry. Perform the same signature normalization at upload time, or persist the detected type instead of trusting the declaration.

AGENTS.md reference: src/AGENTS.md:L58-L58

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants