Skip to content

fix: finalize convex optimization remediations#44

Merged
Simplereally merged 4 commits intomainfrom
feat/convex-cost-phases
Mar 11, 2026
Merged

fix: finalize convex optimization remediations#44
Simplereally merged 4 commits intomainfrom
feat/convex-cost-phases

Conversation

@Simplereally
Copy link
Copy Markdown
Owner

@Simplereally Simplereally commented Mar 11, 2026

Summary

  • fix the batch pause/resume lifecycle so resume cannot duplicate in-flight work and correctly restores in-flight tracking
  • add durable settled-item tracking plus focused batch lifecycle regression tests
  • remove the stale single-generation wrapper entrypoint and keep the direct processor path

Validation

  • bunx convex codegen
  • bun run test convex/lib/batchGenerationState.test.ts components/studio/batch/batch-action-button.test.tsx hooks/queries/use-batch-generation.test.ts hooks/use-batch-mode.test.ts hooks/queries/use-generate-image.test.tsx
  • bun run typecheck

Summary by CodeRabbit

  • New Features

    • Adaptive throttling for batch processing that dynamically adjusts scheduling delays.
    • Dev-mode mock generation to preview generation UX without external API calls.
  • Bug Fixes

    • Prevented duplicate item processing on resume and corrected in-flight count handling.
    • Added hard timeouts and improved retry behavior for external API requests.
    • Resume control now disables while work is in-flight, with explanatory tooltip.
  • UI/UX

    • Improved error messaging and toast reporting for batch operations.
  • Tests

    • Expanded coverage for batch lifecycle and pause/resume scenarios.
  • Documentation

    • Added comprehensive optimization and implementation guidance.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
bloomstudio Ignored Ignored Preview Mar 11, 2026 1:06pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 11, 2026

Warning

Rate limit exceeded

@Simplereally has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 687db6cd-5e5a-46de-9a78-8cce198c38d0

📥 Commits

Reviewing files that changed from the base of the PR and between 4db6f31 and 76ea7dd.

📒 Files selected for processing (1)
  • components/studio/upgrade-modal.test.tsx
📝 Walkthrough

Walkthrough

This pull request refactors the single-generation API (replacing dispatchGeneration with processGeneration in singleGenerationProcessor), introduces adaptive throttling for batch operations, adds hard fetch timeouts with retry logic, implements dev-only mock image generation, and includes comprehensive documentation of optimization findings plus an identified correctness bug in pause/resume logic under pipelined scheduling.

Changes

Cohort / File(s) Summary
Single Generation API
convex/singleGeneration.ts, convex/singleGenerationProcessor.ts, hooks/queries/use-generate-image.ts, hooks/queries/use-generate-image.test.tsx, components/debug/limit-tester.tsx
Removes singleGeneration.dispatchGeneration, adds singleGenerationProcessor.processGeneration and processGenerationInternal, switches call sites/tests to new API, and integrates unified processing flow with dev-mock support and timeout-aware fetches.
Batch Processing & Throttling
convex/batchGeneration.ts, convex/batchProcessor.ts, convex/schema.ts
Adds adaptiveDelayMs and settledItemIndexes to schema, implements adaptive throttling (adjustAdaptiveDelay), integrates fetch-with-timeout and attempt-based retries, refactors scheduling/recording to use transition logic and resume decisions.
Batch State Primitives & Tests
convex/lib/batchGenerationState.ts, convex/lib/batchGenerationState.test.ts
New lifecycle types and functions (BatchLifecycleStatus, ResumeBatchDecision, getResumeBatchDecision, buildRecordBatchItemResultTransition) with unit tests for resume and result-transition behaviors.
Polling / API Helpers
convex/lib/pollinations.ts, convex/lib/index.ts
Adds fetchPollinationsWithTimeout, formatApiErrorText, PollinationsAttemptResult, and RetryKind; re-exports them from lib index.
Batch UI & Hooks
components/studio/batch/batch-action-button.tsx, components/studio/batch/batch-action-button.test.tsx, hooks/use-batch-mode.ts
Disables resume when paused with in-flight work, adds disabled styling/title, test for disabled resume behavior, and improves error message extraction/display on batch start/resume failures.
Tests & Hook Updates
hooks/queries/use-generate-image.test.tsx, components/debug/limit-tester.tsx
Update mocks and tests to call singleGenerationProcessor.processGeneration instead of the removed dispatchGeneration; rename mock handlers accordingly.
Docs & Analysis
convex-optimization-branch-review.md, convex-optimiziations.md
Adds comprehensive branch review and optimization plan documenting findings (including P1/P2 correctness bugs) and phased fixes for retry rescheduling, timeouts, and batching improvements.
Misc / Config
lib/config/models.test.ts, package.json
Updates model list expectations (adds dirtberry) and small package/test adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ConvexAction as Convex Action
    participant Processor as Generation Processor
    participant Poll as Pollinations Fetch
    participant R2 as R2 Storage
    participant DB as Convex DB

    Client->>ConvexAction: call singleGenerationProcessor.processGeneration(generationId, apiKey)
    ConvexAction->>Processor: validate & start processing
    alt Dev mock enabled
        Processor->>Processor: generate placeholder image
    else Production
        Processor->>Poll: fetchPollinationsWithTimeout(url, apiKey)
        alt Success
            Poll-->>Processor: image bytes/response
        else Retryable failure
            Poll-->>Processor: error + retryKind
            Processor->>DB: adjustAdaptiveDelay(batchId, outcome="throttle"/"transient_error")
            Processor->>ConvexAction: schedule retry (attempt+1)
        else Permanent failure
            Poll-->>Processor: permanent error
            Processor->>DB: recordBatchItemResult(failure)
        end
    end
    Processor->>R2: upload image (on success)
    Processor->>DB: recordBatchItemResult(success), adjustAdaptiveDelay(success)
    Processor-->>Client: processing result / status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 I hopped through queues and nibbled the queue,

I taught the throttle to dance when it’s due,
Timeouts blink softly, mock pixels shine bright,
Pause and resume now behave through the night,
A carrot of code — lean, fast, and light. 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: finalizing Convex optimization fixes for batch pause/resume lifecycle, settling item tracking, and removing stale API wrappers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/convex-cost-phases

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

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

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: d9220d2556

ℹ️ 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 thread convex/batchGeneration.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
convex/batchGeneration.ts (1)

722-728: ⚠️ Potential issue | 🟠 Major

Bug: resumeDecision.itemIndex is computed but not used.

getResumeBatchDecision computes the correct itemIndex that accounts for already-settled items, but resumeBatchJob ignores it and uses batchJob.currentIndex instead:

if (batchJob.currentIndex < batchJob.totalCount) {
    await ctx.scheduler.runAfter(0, internal.batchProcessor.processBatchItem, {
        batchJobId: args.batchJobId,
        itemIndex: batchJob.currentIndex,  // Should be resumeDecision.itemIndex
    })
}

This undermines the settled-item tracking and could lead to duplicate item processing on resume.

🐛 Suggested fix
-        // Schedule the next item for processing
-        if (batchJob.currentIndex < batchJob.totalCount) {
+        // Schedule the next item for processing using the decision's computed index
+        if (resumeDecision.itemIndex !== null) {
             await ctx.scheduler.runAfter(0, internal.batchProcessor.processBatchItem, {
                 batchJobId: args.batchJobId,
-                itemIndex: batchJob.currentIndex,
+                itemIndex: resumeDecision.itemIndex,
             })
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/batchGeneration.ts` around lines 722 - 728, resumeBatchJob is
scheduling the next item using batchJob.currentIndex instead of the adjusted
index returned by getResumeBatchDecision; change the scheduled payload to use
resumeDecision.itemIndex so settled items are skipped. Locate where
resumeBatchJob calls ctx.scheduler.runAfter(...,
internal.batchProcessor.processBatchItem, { batchJobId: args.batchJobId,
itemIndex: batchJob.currentIndex }) and replace batchJob.currentIndex with the
computed resumeDecision.itemIndex (from getResumeBatchDecision) and ensure the
surrounding conditional uses resumeDecision.itemIndex when deciding to schedule
the next item.
🧹 Nitpick comments (5)
convex-optimiziations.md (1)

9-9: Stale reference to removed API.

This line references singleGeneration.dispatchGeneration, which is being removed in this PR. Consider updating to reflect the new singleGenerationProcessor.processGeneration path, or note that this was the prior state being optimized.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex-optimiziations.md` at line 9, Update the stale API reference: replace
mentions of singleGeneration.dispatchGeneration with the new
singleGenerationProcessor.processGeneration symbol (or explicitly state that
dispatchGeneration was the prior implementation being optimized away) so the doc
reflects the current code paths; update the text around the Convex/ctx.runAction
comment to reference singleGenerationProcessor.processGeneration and remove or
rephrase the note about crossing runtimes accordingly.
hooks/queries/use-generate-image.test.tsx (1)

126-128: Minor indentation inconsistency.

The singleGenerationProcessor object has extra indentation compared to sibling properties like singleGeneration and generatedImages.

🔧 Suggested fix
         },
-            singleGenerationProcessor: {
-                processGeneration: "singleGenerationProcessor.processGeneration",
-            },
+        singleGenerationProcessor: {
+            processGeneration: "singleGenerationProcessor.processGeneration",
+        },
         generatedImages: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/queries/use-generate-image.test.tsx` around lines 126 - 128, The object
key `singleGenerationProcessor` is over-indented compared to its sibling
properties (`singleGeneration`, `generatedImages`); adjust the indentation of
the `singleGenerationProcessor` block (and its nested `processGeneration:
"singleGenerationProcessor.processGeneration"`) so it aligns with the other
top-level properties to maintain consistent formatting.
convex/singleGenerationProcessor.ts (1)

172-461: Consider extracting helper functions for readability.

runGenerationProcessor is ~290 lines handling validation, API calls, cropping, upload, persistence, and error recovery. While the sequential flow is logical, extracting some phases into named helpers would improve readability and testability:

  • Validation + cancellation checks
  • Pollinations API call with retry scheduling
  • Post-processing (crop + upload)
  • Persistence and completion

This is optional given the function's clear linear structure, but would help as the codebase grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/singleGenerationProcessor.ts` around lines 172 - 461, The
runGenerationProcessor function is very large and should be split into clear
helper functions to improve readability and testability: extract the
validation/ownership/cancellation logic into a helper (e.g.,
validateAndClaimGeneration using runGenerationProcessor, getGeneration,
isCancelled, claimPendingGeneration), extract the Pollinations request + retry
scheduling into a helper (e.g., callPollinationsWithRetry using
buildPollinationsUrl, fetchPollinationsWithTimeout, and scheduler.runAfter),
extract post-processing (crop + prepare upload buffer) into a helper (e.g.,
processImageBuffer using shouldCropDirtberry, cropDirtberryImageBuffer), and
extract persistence/completion into a helper (e.g., persistGenerationResult
using uploadMediaWithThumbnail, storeGeneratedImage, updateGenerationStatus);
move the corresponding try/catch and cancellation checks into these helpers and
call them sequentially from runGenerationProcessor so the top-level function
becomes a concise orchestrator.
convex/lib/batchGenerationState.test.ts (1)

39-53: Test coverage gap: sparse settled indexes not covered.

This test only verifies the case where settledItemIndexes are consecutive from 0 ([0, 1, 2, 3, 4]). However, the implementation in getResumeBatchDecision only increments by 1 when the current index is settled—it does not scan forward through multiple consecutive settled items.

For sparse settled sets (e.g., settledItemIndexes = [0, 1, 5] with currentIndex = 0), the function would return itemIndex = 1, but index 1 is already settled. This could lead to duplicate scheduling.

Consider adding a test case with non-consecutive settled indexes to verify the implementation handles or rejects this scenario correctly.

💡 Suggested additional test case
it("handles sparse settled indexes correctly", () => {
    // If indexes 0 and 2 are settled but 1 is not, should return 1
    expect(
        getResumeBatchDecision({
            currentIndex: 0,
            totalCount: 10,
            inFlightCount: 0,
            settledItemIndexes: [0, 2],
        })
    ).toEqual({
        canSchedule: true,
        itemIndex: 1, // or should scan to find first unsettled?
        nextInFlightCount: 1,
        reason: null,
    })
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/lib/batchGenerationState.test.ts` around lines 39 - 53, The current
behavior of getResumeBatchDecision can return an itemIndex that is already in
settledItemIndexes when settled indexes are sparse; update
getResumeBatchDecision to treat settledItemIndexes as a set and advance
itemIndex repeatedly (starting from currentIndex) until you find the first index
< totalCount that is not in settledItemIndexes (or determine canSchedule=false
if none), adjusting nextInFlightCount accordingly, and add a unit test in
batchGenerationState.test.ts (e.g., "handles sparse settled indexes correctly")
that asserts for inputs like currentIndex=0, totalCount=10, inFlightCount=0,
settledItemIndexes=[0,2] that the returned itemIndex is the first unsettled
index (1) and canSchedule/nextInFlightCount are correct.
convex/batchProcessor.ts (1)

67-160: Code duplication with singleGenerationProcessor.ts.

formatApiErrorText, PollinationsAttemptResult type, and fetchPollinationsWithTimeout are nearly identical to their counterparts in singleGenerationProcessor.ts. The only difference is that batchProcessor includes a retryKind field.

Consider extracting these into a shared module (e.g., convex/lib/pollinationsClient.ts) to reduce duplication and ensure consistent behavior.

♻️ Suggested shared module structure
// convex/lib/pollinationsClient.ts
export type RetryKind = "throttle" | "transient_error" | "permanent"

export type PollinationsAttemptResult =
    | { success: true; response: Response }
    | {
        success: false
        errorMessage: string
        statusCode?: number
        retryable: boolean
        retryKind?: RetryKind // optional for single-gen, required for batch
    }

export function formatApiErrorText(rawErrorText: string): string { ... }

export async function fetchPollinationsWithTimeout(
    url: string,
    apiKey: string,
    timeoutMs: number,
    logger: string,
    includeRetryKind?: boolean
): Promise<PollinationsAttemptResult> { ... }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/batchProcessor.ts` around lines 67 - 160, Extract the duplicated types
and functions into a shared module (e.g., convex/lib/pollinationsClient.ts):
move RetryKind, PollinationsAttemptResult, formatApiErrorText, and
fetchPollinationsWithTimeout into that file; change PollinationsAttemptResult’s
failed variant to make retryKind optional (retryKind?: RetryKind) so
singleGenerationProcessor can ignore it, and add an optional parameter to
fetchPollinationsWithTimeout (e.g., includeRetryKind?: boolean) that controls
whether the returned result includes retryKind for batchProcessor; then update
batchProcessor.ts and singleGenerationProcessor.ts to import RetryKind,
PollinationsAttemptResult, formatApiErrorText, and fetchPollinationsWithTimeout
from the new module and remove the duplicated implementations (ensure
fetchPollinationsWithTimeout still computes retryable and retryKind internally
and only attaches retryKind when includeRetryKind is true).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@convex/lib/batchGenerationState.ts`:
- Around line 26-30: getResumeBatchDecision currently only skips one settled
index which can reschedule already-settled items; replace the single-step logic
that computes itemIndex with a scan to the first unsettled index: use the
settledItemIndexes (defaulting to []) and advance from state.currentIndex while
settledItemIndexes.includes(index) to find the first index not settled, then use
that as itemIndex (handle empty/default arrays and preserve use of
state.currentIndex when nothing is settled). Update the logic in
batchGenerationState.ts where settledItemIndexes and itemIndex are computed to
perform this loop so duplicate scheduling is prevented.

In `@hooks/use-batch-mode.ts`:
- Around line 304-307: The catch block that handles errors from resumeBatchJob
should not blindly cast the thrown value to Error; instead detect ConvexError
shape and extract a usable message before calling showErrorToast. Update the
catch to check if error has a 'data' object with a 'message' string (the
ConvexError payload) or fallback to error.message or String(error), then pass
that extracted message (or wrap it in an Error) to showErrorToast; remove the
"as Error" cast and ensure console.error logs the original error and the
extracted message for clarity. Reference resumeBatchJob, ConvexError
(error.data.message), showErrorToast, and the catch block in useBatchMode.

---

Outside diff comments:
In `@convex/batchGeneration.ts`:
- Around line 722-728: resumeBatchJob is scheduling the next item using
batchJob.currentIndex instead of the adjusted index returned by
getResumeBatchDecision; change the scheduled payload to use
resumeDecision.itemIndex so settled items are skipped. Locate where
resumeBatchJob calls ctx.scheduler.runAfter(...,
internal.batchProcessor.processBatchItem, { batchJobId: args.batchJobId,
itemIndex: batchJob.currentIndex }) and replace batchJob.currentIndex with the
computed resumeDecision.itemIndex (from getResumeBatchDecision) and ensure the
surrounding conditional uses resumeDecision.itemIndex when deciding to schedule
the next item.

---

Nitpick comments:
In `@convex-optimiziations.md`:
- Line 9: Update the stale API reference: replace mentions of
singleGeneration.dispatchGeneration with the new
singleGenerationProcessor.processGeneration symbol (or explicitly state that
dispatchGeneration was the prior implementation being optimized away) so the doc
reflects the current code paths; update the text around the Convex/ctx.runAction
comment to reference singleGenerationProcessor.processGeneration and remove or
rephrase the note about crossing runtimes accordingly.

In `@convex/batchProcessor.ts`:
- Around line 67-160: Extract the duplicated types and functions into a shared
module (e.g., convex/lib/pollinationsClient.ts): move RetryKind,
PollinationsAttemptResult, formatApiErrorText, and fetchPollinationsWithTimeout
into that file; change PollinationsAttemptResult’s failed variant to make
retryKind optional (retryKind?: RetryKind) so singleGenerationProcessor can
ignore it, and add an optional parameter to fetchPollinationsWithTimeout (e.g.,
includeRetryKind?: boolean) that controls whether the returned result includes
retryKind for batchProcessor; then update batchProcessor.ts and
singleGenerationProcessor.ts to import RetryKind, PollinationsAttemptResult,
formatApiErrorText, and fetchPollinationsWithTimeout from the new module and
remove the duplicated implementations (ensure fetchPollinationsWithTimeout still
computes retryable and retryKind internally and only attaches retryKind when
includeRetryKind is true).

In `@convex/lib/batchGenerationState.test.ts`:
- Around line 39-53: The current behavior of getResumeBatchDecision can return
an itemIndex that is already in settledItemIndexes when settled indexes are
sparse; update getResumeBatchDecision to treat settledItemIndexes as a set and
advance itemIndex repeatedly (starting from currentIndex) until you find the
first index < totalCount that is not in settledItemIndexes (or determine
canSchedule=false if none), adjusting nextInFlightCount accordingly, and add a
unit test in batchGenerationState.test.ts (e.g., "handles sparse settled indexes
correctly") that asserts for inputs like currentIndex=0, totalCount=10,
inFlightCount=0, settledItemIndexes=[0,2] that the returned itemIndex is the
first unsettled index (1) and canSchedule/nextInFlightCount are correct.

In `@convex/singleGenerationProcessor.ts`:
- Around line 172-461: The runGenerationProcessor function is very large and
should be split into clear helper functions to improve readability and
testability: extract the validation/ownership/cancellation logic into a helper
(e.g., validateAndClaimGeneration using runGenerationProcessor, getGeneration,
isCancelled, claimPendingGeneration), extract the Pollinations request + retry
scheduling into a helper (e.g., callPollinationsWithRetry using
buildPollinationsUrl, fetchPollinationsWithTimeout, and scheduler.runAfter),
extract post-processing (crop + prepare upload buffer) into a helper (e.g.,
processImageBuffer using shouldCropDirtberry, cropDirtberryImageBuffer), and
extract persistence/completion into a helper (e.g., persistGenerationResult
using uploadMediaWithThumbnail, storeGeneratedImage, updateGenerationStatus);
move the corresponding try/catch and cancellation checks into these helpers and
call them sequentially from runGenerationProcessor so the top-level function
becomes a concise orchestrator.

In `@hooks/queries/use-generate-image.test.tsx`:
- Around line 126-128: The object key `singleGenerationProcessor` is
over-indented compared to its sibling properties (`singleGeneration`,
`generatedImages`); adjust the indentation of the `singleGenerationProcessor`
block (and its nested `processGeneration:
"singleGenerationProcessor.processGeneration"`) so it aligns with the other
top-level properties to maintain consistent formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e8ea68c-af6e-4792-b063-942d6325d2c7

📥 Commits

Reviewing files that changed from the base of the PR and between 3676d7d and d9220d2.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (15)
  • components/debug/limit-tester.tsx
  • components/studio/batch/batch-action-button.test.tsx
  • components/studio/batch/batch-action-button.tsx
  • convex-optimization-branch-review.md
  • convex-optimiziations.md
  • convex/batchGeneration.ts
  • convex/batchProcessor.ts
  • convex/lib/batchGenerationState.test.ts
  • convex/lib/batchGenerationState.ts
  • convex/schema.ts
  • convex/singleGeneration.ts
  • convex/singleGenerationProcessor.ts
  • hooks/queries/use-generate-image.test.tsx
  • hooks/queries/use-generate-image.ts
  • hooks/use-batch-mode.ts

Comment thread convex/lib/batchGenerationState.ts Outdated
Comment thread hooks/use-batch-mode.ts
…ogic

- Move `fetchPollinationsWithTimeout` and `formatApiErrorText` to pollinations lib
- Add optional `retryKind` parameter to `fetchPollinationsWithTimeout`
- Export `PollinationsAttemptResult` and `RetryKind` types from lib index
- Fix batch resume to skip all settled indexes, not just current
- Update `resumeBatchJob` to use `itemIndex` from resume decision
- Add test coverage for sparse settled indexes in batch generation
Copy link
Copy Markdown

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

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: 4db6f31291

ℹ️ 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 +27 to +30
let itemIndex = state.currentIndex

while (settledItemIndexes.includes(itemIndex) && itemIndex < state.totalCount) {
itemIndex += 1
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 from earliest unsettled item index

getResumeBatchDecision starts scanning at currentIndex, which assumes every lower index is already settled. That assumption is broken by the new scheduled-retry flow in batchProcessor.processBatchItem: if an item fails transiently after a pause is requested, its retry is queued, later exits early on paused status, and leaves that lower itemIndex unsettled while currentIndex has already advanced. Resuming from currentIndex then skips the missed item permanently, so completedCount + failedCount can stay below totalCount and the batch can get stuck instead of completing.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 11, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/Simplereally/bloomstudio/issues/comments/4038917140","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nThis pull request refactors the single-generation API (replacing `dispatchGeneration` with `processGeneration` in singleGenerationProcessor), introduces adaptive throttling for batch operations, adds hard fetch timeouts with retry logic, implements dev-only mock image generation, and includes comprehensive documentation of optimization findings plus an identified correctness bug in pause/resume logic under pipelined scheduling.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Single Generation API** <br> `convex/singleGeneration.ts`, `convex/singleGenerationProcessor.ts`, `hooks/queries/use-generate-image.ts`, `hooks/queries/use-generate-image.test.tsx`, `components/debug/limit-tester.tsx`|Removes `singleGeneration.dispatchGeneration`, adds `singleGenerationProcessor.processGeneration` and `processGenerationInternal`, switches call sites/tests to new API, and integrates unified processing flow with dev-mock support and timeout-aware fetches.|\n|**Batch Processing & Throttling** <br> `convex/batchGeneration.ts`, `convex/batchProcessor.ts`, `convex/schema.ts`|Adds adaptiveDelayMs and settledItemIndexes to schema, implements adaptive throttling (adjustAdaptiveDelay), integrates fetch-with-timeout and attempt-based retries, refactors scheduling/recording to use transition logic and resume decisions.|\n|**Batch State Primitives & Tests** <br> `convex/lib/batchGenerationState.ts`, `convex/lib/batchGenerationState.test.ts`|New lifecycle types and functions (`BatchLifecycleStatus`, `ResumeBatchDecision`, `getResumeBatchDecision`, `buildRecordBatchItemResultTransition`) with unit tests for resume and result-transition behaviors.|\n|**Polling / API Helpers** <br> `convex/lib/pollinations.ts`, `convex/lib/index.ts`|Adds `fetchPollinationsWithTimeout`, `formatApiErrorText`, `PollinationsAttemptResult`, and `RetryKind`; re-exports them from lib index.|\n|**Batch UI & Hooks** <br> `components/studio/batch/batch-action-button.tsx`, `components/studio/batch/batch-action-button.test.tsx`, `hooks/use-batch-mode.ts`|Disables resume when paused with in-flight work, adds disabled styling/title, test for disabled resume behavior, and improves error message extraction/display on batch start/resume failures.|\n|**Tests & Hook Updates** <br> `hooks/queries/use-generate-image.test.tsx`, `components/debug/limit-tester.tsx`|Update mocks and tests to call `singleGenerationProcessor.processGeneration` instead of the removed `dispatchGeneration`; rename mock handlers accordingly.|\n|**Docs & Analysis** <br> `convex-optimization-branch-review.md`, `convex-optimiziations.md`|Adds comprehensive branch review and optimization plan documenting findings (including P1/P2 correctness bugs) and phased fixes for retry rescheduling, timeouts, and batching improvements.|\n|**Misc / Config** <br> `lib/config/models.test.ts`, `package.json`|Updates model list expectations (adds `dirtberry`) and small package/test adjustments.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant ConvexAction as Convex Action\n    participant Processor as Generation Processor\n    participant Poll as Pollinations Fetch\n    participant R2 as R2 Storage\n    participant DB as Convex DB\n\n    Client->>ConvexAction: call singleGenerationProcessor.processGeneration(generationId, apiKey)\n    ConvexAction->>Processor: validate & start processing\n    alt Dev mock enabled\n        Processor->>Processor: generate placeholder image\n    else Production\n        Processor->>Poll: fetchPollinationsWithTimeout(url, apiKey)\n        alt Success\n            Poll-->>Processor: image bytes/response\n        else Retryable failure\n            Poll-->>Processor: error + retryKind\n            Processor->>DB: adjustAdaptiveDelay(batchId, outcome=\"throttle\"/\"transient_error\")\n            Processor->>ConvexAction: schedule retry (attempt+1)\n        else Permanent failure\n            Poll-->>Processor: permanent error\n            Processor->>DB: recordBatchItemResult(failure)\n        end\n    end\n    Processor->>R2: upload image (on success)\n    Processor->>DB: recordBatchItemResult(success), adjustAdaptiveDelay(success)\n    Processor-->>Client: processing result / status\n```\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~50 minutes\n\n## Possibly related PRs\n\n- Simplereally/bloomstudio#18: Mirrors the API refactor replacing `singleGeneration.dispatchGeneration` with `singleGenerationProcessor.processGeneration` (server + client updates).\n- Simplereally/bloomstudio#43: Related changes around Dirtberry model support and image-cropping logic used in generation processing and batch flows.\n- Simplereally/bloomstudio#30: Touches single-generation flow and client hook bindings, aligning with the new `processGeneration` entrypoint.\n\n## Poem\n\n> 🐇 I hopped through queues and nibbled the queue,  \n> > I taught the throttle to dance when it’s due,  \n> > Timeouts blink softly, mock pixels shine bright,  \n> > Pause and resume now behave through the night,  \n> > A carrot of code — lean, fast, and light. 🌿\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 2 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n<details>\n<summary>✅ Passed checks (2 passed)</summary>\n\n|     Check name    | Status   | Explanation                                                                                                                                                                    |\n| :---------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                    |\n|    Title check    | ✅ Passed | The title accurately reflects the main change: finalizing Convex optimization fixes for batch pause/resume lifecycle, settling item tracking, and removing stale API wrappers. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings (stacked PR)\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> 📝 Generate docstrings (commit on current branch)\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `feat/convex-cost-phases`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Simplereally/bloomstudio&utm_content=44)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"retries":3,"retryAfter":16}}}

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
lib/config/models.test.ts (1)

190-201: Consolidate these into one exact model-id assertion.

This block is now a second source of truth for image models: Line 194 hard-codes the count, and the membership list here already diverges from expectedImageModels above because dirtberry was only added in this section. Line 190 also checks counts only, so duplicate/misaligned model.id values could still slip through. Prefer one shared expected list and compare exact sets.

♻️ Suggested refactor
-        it("should have all model IDs", () => {
-            expect(ALL_MODEL_IDS.length).toBe(IMAGE_MODEL_IDS.length + VIDEO_MODEL_IDS.length)
-        })
-
-        it("should have correct image model IDs", () => {
-            expect(IMAGE_MODEL_IDS.length).toBe(16)
-            expect(IMAGE_MODEL_IDS).toContain("zimage")
-            expect(IMAGE_MODEL_IDS).toContain("gptimage")
-            expect(IMAGE_MODEL_IDS).toContain("flux")
-            expect(IMAGE_MODEL_IDS).toContain("klein")
-            expect(IMAGE_MODEL_IDS).toContain("imagen-4")
-            expect(IMAGE_MODEL_IDS).toContain("grok-imagine")
-            expect(IMAGE_MODEL_IDS).toContain("dirtberry")
-            expect(IMAGE_MODEL_IDS).toContain("flux-2-dev")
-            expect(IMAGE_MODEL_IDS).not.toContain("veo")
-        })
+        const expectedImageModelIds = [
+            "zimage",
+            "turbo",
+            "gptimage",
+            "gptimage-large",
+            "seedream",
+            "kontext",
+            "flux",
+            "nanobanana",
+            "seedream-pro",
+            "nanobanana-pro",
+            "klein",
+            "klein-large",
+            "imagen-4",
+            "grok-imagine",
+            "dirtberry",
+            "flux-2-dev",
+        ]
+
+        it("should have correct image model IDs", () => {
+            expect(new Set(IMAGE_MODEL_IDS)).toEqual(new Set(expectedImageModelIds))
+        })
+
+        it("should have all model IDs", () => {
+            expect(new Set(ALL_MODEL_IDS)).toEqual(
+                new Set([...IMAGE_MODEL_IDS, ...VIDEO_MODEL_IDS]),
+            )
+        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config/models.test.ts` around lines 190 - 201, The test duplicates image
model expectations across IMAGE_MODEL_IDS and a hard-coded count; replace the
many membership checks and the length assertion with a single exact equality
assertion against the canonical expectedImageModels array (or rename that
canonical list if needed) — update the "should have correct image model IDs"
spec to assert that IMAGE_MODEL_IDS (or Array.from(IMAGE_MODEL_IDS) if it's a
Set) equals expectedImageModels as a set/array (compare sorted arrays or use a
set-equality helper) and remove the separate expect(IMAGE_MODEL_IDS.length).
Ensure the test references the single source of truth variable
(expectedImageModels) so additions like "dirtberry" are not duplicated in
another place.
convex/lib/batchGenerationState.test.ts (1)

1-6: Consider: @testing-library/jest-dom/vitest import may be unnecessary.

This import provides DOM matchers (like toBeInTheDocument), but these tests only use Vitest's built-in toEqual and toBe. The import is harmless but adds unused setup overhead.

🧹 Optional cleanup
-import "@testing-library/jest-dom/vitest"
 import { describe, expect, it } from "vitest"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/lib/batchGenerationState.test.ts` around lines 1 - 6, The test file
imports "@testing-library/jest-dom/vitest" which is unused; remove that import
line at the top so only Vitest's utilities are imported and avoid unnecessary
DOM matcher setup (update the top imports that currently include
buildRecordBatchItemResultTransition and getResumeBatchDecision to only import
from "vitest" and "./batchGenerationState" as shown, ensuring no other tests
rely on jest-dom matchers).
convex/singleGenerationProcessor.ts (1)

44-68: Dev mock configuration is duplicated from batchProcessor.ts.

The constants and shouldUseDevGenerationMock function are nearly identical between this file and convex/batchProcessor.ts:43-54. Consider extracting to a shared utility in convex/lib/ if these need to stay in sync.

💡 Optional: Extract shared dev mock utilities

Create a new file convex/lib/devMock.ts:

export const ENABLE_DEV_GENERATION_MOCK = process.env.CONVEX_ENABLE_DEV_GENERATION_MOCK === "true"
export const MOCK_PNG_BASE64 =
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7k6xwAAAAASUVORK5CYII="
export const MOCK_IMAGE_BUFFER = Buffer.from(MOCK_PNG_BASE64, "base64")

export function shouldUseDevGenerationMock(isVideoRequest: boolean): boolean {
    const deployment = process.env.CONVEX_DEPLOYMENT ?? ""
    const isDevDeployment = deployment.startsWith("dev:")
    return ENABLE_DEV_GENERATION_MOCK && isDevDeployment && !isVideoRequest
}

Then import from both processors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/singleGenerationProcessor.ts` around lines 44 - 68, Duplicate dev-mock
config (ENABLE_DEV_GENERATION_MOCK, MOCK_PNG_BASE64, MOCK_IMAGE_BUFFER) and
shouldUseDevGenerationMock are defined here and in batchProcessor; extract them
into a single shared module (e.g., convex/lib/devMock) that exports those
constants and the shouldUseDevGenerationMock function, update
singleGenerationProcessor to import and use them, and remove the duplicated
local definitions so both processors consume the shared utility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@convex/lib/batchGenerationState.test.ts`:
- Around line 1-6: The test file imports "@testing-library/jest-dom/vitest"
which is unused; remove that import line at the top so only Vitest's utilities
are imported and avoid unnecessary DOM matcher setup (update the top imports
that currently include buildRecordBatchItemResultTransition and
getResumeBatchDecision to only import from "vitest" and "./batchGenerationState"
as shown, ensuring no other tests rely on jest-dom matchers).

In `@convex/singleGenerationProcessor.ts`:
- Around line 44-68: Duplicate dev-mock config (ENABLE_DEV_GENERATION_MOCK,
MOCK_PNG_BASE64, MOCK_IMAGE_BUFFER) and shouldUseDevGenerationMock are defined
here and in batchProcessor; extract them into a single shared module (e.g.,
convex/lib/devMock) that exports those constants and the
shouldUseDevGenerationMock function, update singleGenerationProcessor to import
and use them, and remove the duplicated local definitions so both processors
consume the shared utility.

In `@lib/config/models.test.ts`:
- Around line 190-201: The test duplicates image model expectations across
IMAGE_MODEL_IDS and a hard-coded count; replace the many membership checks and
the length assertion with a single exact equality assertion against the
canonical expectedImageModels array (or rename that canonical list if needed) —
update the "should have correct image model IDs" spec to assert that
IMAGE_MODEL_IDS (or Array.from(IMAGE_MODEL_IDS) if it's a Set) equals
expectedImageModels as a set/array (compare sorted arrays or use a set-equality
helper) and remove the separate expect(IMAGE_MODEL_IDS.length). Ensure the test
references the single source of truth variable (expectedImageModels) so
additions like "dirtberry" are not duplicated in another place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 801a11a5-ee9c-4dfc-8a27-a9d4b241dab2

📥 Commits

Reviewing files that changed from the base of the PR and between d9220d2 and 4db6f31.

📒 Files selected for processing (10)
  • convex/batchGeneration.ts
  • convex/batchProcessor.ts
  • convex/lib/batchGenerationState.test.ts
  • convex/lib/batchGenerationState.ts
  • convex/lib/index.ts
  • convex/lib/pollinations.ts
  • convex/singleGenerationProcessor.ts
  • hooks/queries/use-generate-image.test.tsx
  • hooks/use-batch-mode.ts
  • lib/config/models.test.ts

@Simplereally Simplereally merged commit ed3976a into main Mar 11, 2026
3 checks passed
@Simplereally Simplereally deleted the feat/convex-cost-phases branch March 11, 2026 13:06
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.

1 participant