Skip to content

feat(concurrency): bullmq based concurrency control system#3605

Merged
icecrasher321 merged 20 commits intostagingfrom
feat/conc-control
Mar 27, 2026
Merged

feat(concurrency): bullmq based concurrency control system#3605
icecrasher321 merged 20 commits intostagingfrom
feat/conc-control

Conversation

@icecrasher321
Copy link
Copy Markdown
Collaborator

@icecrasher321 icecrasher321 commented Mar 16, 2026

Summary

  • BullMQ based concurrency control system for executions currently running in line [manual execs excluded]. Can tune limits based on resources.

  • Overall admin gates to prevent rate limiting services based crashes.

Type of Change

  • New feature
  • Other: Reliability

Testing

Tested manually under different configurations, added extensive test suite

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 16, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 26, 2026 9:29pm

Request Review

@icecrasher321 icecrasher321 marked this pull request as ready for review March 16, 2026 03:56
@cursor
Copy link
Copy Markdown

cursor bot commented Mar 16, 2026

PR Summary

High Risk
High risk because it rewires core workflow/webhook/schedule execution queuing and status reporting to a new BullMQ + workspace-dispatch pipeline with new capacity limits and fallback behavior; mistakes could drop/strand jobs or incorrectly reject traffic.

Overview
Introduces a BullMQ-backed workspace dispatch layer that queues execution jobs per workspace/lane, enforces global/workspace queue caps, leases concurrency slots, and reconciles BullMQ state back into dispatch records (with in-memory or Redis-backed dispatch storage).

Updates workflow execution, scheduled execution, and webhook trigger APIs to enqueue through enqueueWorkspaceDispatch when BullMQ is enabled, adds explicit capacity responses (429 admission gate, 503 dispatch-queue-full with Retry-After), and changes /api/jobs/[jobId] to be dispatcher-aware via presentDispatchOrJobStatus.

Adds billing-driven workspace concurrency limits (plan defaults + enterprise metadata override) with caching, extends enterprise billing metadata parsing with zod (including concurrency limit), and documents/markets the new concurrency limits in docs and pricing UI.

Replaces the async-jobs Redis backend with a BullMQ backend and adjusts inline-execution semantics so only the database fallback executes inline.

Written by Cursor Bugbot for commit 53733e4. This will update automatically on new commits. Configure here.

@icecrasher321 icecrasher321 requested a review from Sg312 March 16, 2026 03:56
@icecrasher321 icecrasher321 changed the title feat(concurrency): bullmq based queueing system feat(concurrency): bullmq based concurrency control system Mar 16, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR introduces a comprehensive BullMQ-based queuing and concurrency control system for workflow, webhook, and schedule executions. It replaces the previous Redis/database job queue backends with BullMQ queues backed by Redis, adds a per-workspace fairness dispatcher (with a Lua-script-based atomic claim mechanism), a lease-based concurrency limit (keyed to billing plan), an in-process admission gate for external API requests, and a standalone worker process to consume jobs. It also adds a buffered SSE stream for non-manual executions that now run asynchronously through the dispatch queue. The change is substantial (~5900 lines added) and represents a foundational infrastructure improvement for reliability and rate-limiting protection.

Key changes and issues found:

  • Misleading variable name in schedule route (apps/sim/app/api/schedules/execute/route.ts, lines 118–123): workspaceId is assigned the getWorkflowById function, not a workspace ID string. The code works at runtime but is extremely confusing and should be renamed (e.g. getWorkflowByIdFn).

  • GLOBAL_DEPTH_KEY double-decrement race (apps/sim/lib/core/workspace-dispatch/redis-store.ts): markDispatchJobCompleted / markDispatchJobFailed unconditionally call DECR on GLOBAL_DEPTH_KEY. If the reconciler and the BullMQ worker both process the same job's completion concurrently, the counter is decremented twice. The Math.max(0, …) guard and periodic reconcileGlobalQueueDepth mitigate the visible impact, but a guard that skips the decrement when the record is already terminal would be more correct.

  • Non-atomic BullMQ add + dispatch record update in finalizeAdmittedJob (apps/sim/lib/core/workspace-dispatch/planner.ts): The BullMQ job is added before the dispatch record is marked admitted. If markDispatchJobAdmitted fails, an orphaned BullMQ job exists with a released lease, creating a window where the job could be executed while the reconciler simultaneously restores the dispatch record to waiting. Reversing the order (mark admitted first, then add to BullMQ) makes the failure mode fully safe for the reconciler to recover.

  • Long-polling HTTP handler (apps/sim/app/api/workflows/[id]/execute/route.ts): waitForDispatchJob can block the HTTP handler for up to 5.5 minutes. This is safe on dedicated servers but will be silently dropped by many load balancers/proxies with shorter idle timeouts. Infrastructure timeout requirements should be documented.

  • Per-process admission gate (apps/sim/lib/core/admission/gate.ts): The inflight counter is module-local. In a horizontally scaled deployment the effective global limit is n_pods × ADMISSION_GATE_MAX_INFLIGHT. Operators should be guided to set the env-var to global_limit / n_pods.

Confidence Score: 3/5

  • PR introduces valuable infrastructure but has a few logic issues around counter correctness, partial-failure atomicity, and infrastructure timeout assumptions that should be resolved before production rollout.
  • The core design (Lua-script atomic claim, lease-based concurrency, BullMQ workers) is well-thought-out and the test suite is comprehensive. However, the non-atomic finalizeAdmittedJob step creates a real (if narrow) window for duplicate execution, the GLOBAL_DEPTH_KEY double-decrement weakens the global gate, and the long-polling HTTP handler will silently fail for users behind standard load balancers. These issues are fixable but should be addressed before heavy production traffic.
  • Pay close attention to apps/sim/lib/core/workspace-dispatch/planner.ts (operation ordering), apps/sim/lib/core/workspace-dispatch/redis-store.ts (double-decrement guard), and apps/sim/app/api/workflows/[id]/execute/route.ts (long-polling timeout).

Important Files Changed

Filename Overview
apps/sim/lib/core/workspace-dispatch/dispatcher.ts New per-process dispatcher loop that coalesces wakeups and drives workspace admission; process-local state is intentional but should be noted for multi-pod deployments.
apps/sim/lib/core/workspace-dispatch/redis-store.ts Redis-backed dispatch store using a Lua script for atomic job claiming; GLOBAL_DEPTH_KEY can be double-decremented in a race between the reconciler and worker completing the same job.
apps/sim/lib/core/workspace-dispatch/planner.ts Claims next admissible workspace job via a Redis lock; BullMQ add and markDispatchJobAdmitted are not atomic — a partial failure leaves an orphaned BullMQ job that may be double-executed.
apps/sim/lib/core/workspace-dispatch/reconciler.ts Periodic reconciliation of stranded/stuck dispatch jobs; correctly scoped to admitting/admitted/running states, but can race with the worker on markDispatchJobCompleted.
apps/sim/lib/core/workspace-dispatch/worker.ts Lease heartbeat + job lifecycle hooks for BullMQ workers; solid implementation with proper cleanup in finally block.
apps/sim/lib/core/admission/gate.ts Simple in-process admission gate limiting concurrent external API requests; effective global limit equals n_pods × MAX_INFLIGHT which should be documented for operators.
apps/sim/app/api/workflows/[id]/execute/route.ts Refactored to route non-manual executions through workspace dispatch; long-polling waitForDispatchJob (up to 5.5 min) in the HTTP handler risks infrastructure timeout disconnects.
apps/sim/app/api/schedules/execute/route.ts Updated to route scheduled and mothership jobs through workspace dispatch; contains a misleading variable named workspaceId that holds a function reference (getWorkflowById).
apps/sim/lib/billing/workspace-concurrency.ts New module resolving per-workspace concurrency limits from billing plan, with Redis-backed and in-memory cache (60s TTL); clean implementation.
apps/sim/worker/index.ts New standalone BullMQ worker process with configurable concurrency per queue, graceful shutdown, and periodic dispatcher wake/notification sweep intervals.

Sequence Diagram

sequenceDiagram
    participant Client
    participant RouteHandler as API Route Handler
    participant AdmissionGate as Admission Gate (in-process)
    participant Dispatcher as Workspace Dispatcher
    participant RedisStore as Redis Dispatch Store
    participant BullMQ as BullMQ Queue (Redis)
    participant Worker as BullMQ Worker Process
    participant DispatchWorker as Dispatch Worker (worker.ts)

    Client->>RouteHandler: POST /api/workflows/[id]/execute
    RouteHandler->>AdmissionGate: tryAdmit()
    alt At capacity (inflight >= MAX_INFLIGHT)
        AdmissionGate-->>RouteHandler: null
        RouteHandler-->>Client: 429 Too Many Requests
    else Admitted
        AdmissionGate-->>RouteHandler: ticket
        RouteHandler->>Dispatcher: enqueueWorkspaceDispatch(input)
        Dispatcher->>RedisStore: enqueueWorkspaceDispatchJob()
        RedisStore-->>Dispatcher: jobRecord (status=waiting)
        Dispatcher->>Dispatcher: runDispatcherLoop() [void]
        Dispatcher->>RedisStore: popNextWorkspaceId()
        Dispatcher->>RedisStore: claimWorkspaceJob() [Lua script]
        RedisStore-->>Dispatcher: {type: admitted, record, leaseId}
        Dispatcher->>BullMQ: queue.add(jobName, payload, {jobId})
        Dispatcher->>RedisStore: markDispatchJobAdmitted()
        Dispatcher-->>RouteHandler: dispatchJobId
        RouteHandler->>RouteHandler: waitForDispatchJob(id, timeout) [polls 250ms]
        Worker->>BullMQ: picks up job
        Worker->>DispatchWorker: runDispatchedJob(metadata, fn)
        DispatchWorker->>RedisStore: markDispatchJobRunning()
        DispatchWorker->>DispatchWorker: executeQueuedWorkflowJob() / executeWorkflowJob()
        DispatchWorker->>RedisStore: markDispatchJobCompleted(output)
        DispatchWorker->>RedisStore: releaseWorkspaceLease()
        DispatchWorker->>Dispatcher: wakeWorkspaceDispatcher()
        RedisStore-->>RouteHandler: record (status=completed) [via poll]
        RouteHandler->>AdmissionGate: ticket.release()
        RouteHandler-->>Client: 200 JSON result
    end
Loading

Last reviewed commit: be83c97

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Resolved conflict in document-processor.ts by keeping resolveParserExtension
helper with txt fallback for parseBase64Content (matching staging behavior)
and strict mode for parseHttpFile. Made mimeType optional in both
resolveParserExtension and parseHttpFile.

Made-with: Cursor
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@icecrasher321 icecrasher321 merged commit dda012e into staging Mar 27, 2026
11 checks passed
@waleedlatif1 waleedlatif1 deleted the feat/conc-control branch March 28, 2026 07:50
waleedlatif1 pushed a commit that referenced this pull request Mar 28, 2026
* feat(concurrency): bullmq based queueing system

* fix bun lock

* remove manual execs off queues

* address comments

* fix legacy team limits

* cleanup enterprise typing code

* inline child triggers

* fix status check

* address more comments

* optimize reconciler scan

* remove dead code

* add to landing page

* Add load testing framework

* update bullmq

* fix

* fix headless path

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>
waleedlatif1 added a commit that referenced this pull request Mar 28, 2026
…rm (#3824)

* fix(import): dedup workflow name (#3813)

* feat(concurrency): bullmq based concurrency control system (#3605)

* feat(concurrency): bullmq based queueing system

* fix bun lock

* remove manual execs off queues

* address comments

* fix legacy team limits

* cleanup enterprise typing code

* inline child triggers

* fix status check

* address more comments

* optimize reconciler scan

* remove dead code

* add to landing page

* Add load testing framework

* update bullmq

* fix

* fix headless path

---------

Co-authored-by: Theodore Li <teddy@zenobiapay.com>

* fix(linear): add default null for after cursor (#3814)

* fix(knowledge): reject non-alphanumeric file extensions from document names (#3816)

* fix(knowledge): reject non-alphanumeric file extensions from document names

* fix(knowledge): improve error message when extension is non-alphanumeric

* fix(security): SSRF, access control, and info disclosure (#3815)

* fix(security): scope copilot feedback GET endpoint to authenticated user

Add WHERE clause to filter feedback records by the authenticated user's
ID, preventing any authenticated user from reading all users' copilot
interactions, queries, and workflow YAML (IDOR / CWE-639).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): add SSRF validation and genericize network error messages

Prevent SSRF via user-controlled smtpHost by validating with
validateDatabaseHost before creating the nodemailer transporter.
Collapse distinct network error messages (ECONNREFUSED, ECONNRESET,
ETIMEDOUT) into a single generic message to prevent port-state leakage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add SSRF validation to SFTP/SSH and access control to workspace invitations

Add `validateDatabaseHost` checks to SFTP and SSH connection utilities to
block connections to private/reserved IPs and localhost, matching the
existing pattern used by all database tools. Add authorization check to
the workspace invitation GET endpoint so only the invitee or a workspace
admin can view invitation details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(smtp): restore SMTP response code handling for post-connection errors

SMTP 4xx/5xx response codes are application-level errors (invalid
recipient, mailbox full, server error) unrelated to the SSRF hardening
goal. Restore response code differentiation and logging to preserve
actionable user-facing error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): use session email directly instead of extra DB query

Addresses PR review feedback — align with the workspace invitation
route pattern by using session.user.email instead of re-fetching
from the database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix that broke hasExternalApiCredentials return type

Biome auto-fixed `return auth !== null && auth.startsWith(...)` to
`return auth?.startsWith(...)` which returns `boolean | undefined`,
not `boolean`, causing a TypeScript build failure.

* fix(smtp): pin resolved IP to prevent DNS rebinding (TOCTOU)

Use the pre-resolved IP from validateDatabaseHost instead of the
original hostname when creating the nodemailer transporter. Set
servername to the original hostname to preserve TLS SNI validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(security): extract createPinnedLookup helper for DNS rebinding prevention

Extract reusable createPinnedLookup from secureFetchWithPinnedIP so
non-HTTP transports (SSH, SFTP, IMAP) can pin resolved IPs at the
socket level. SMTP route uses host+servername pinning instead since
nodemailer doesn't reliably pass lookup to both secure/plaintext paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): pin IMAP connections to validated resolved IP

Pass the resolved IP from validateDatabaseHost to ImapFlow as host,
with the original hostname as servername for TLS SNI verification.
Closes the DNS TOCTOU rebinding window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(auth): revert lint autofix on hasExternalApiCredentials return type

Also pin SFTP/SSH connections to validated resolved IP to prevent DNS rebinding.

* fix(security): short-circuit admin check when caller is invitee

Skip the hasWorkspaceAdminAccess DB query when the caller is already
the invitee, avoiding an unnecessary round-trip. Aligns with the org
invitation route pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(worker): dockerfile + helm updates (#3818)

* fix(worker): dockerfile + helm updates

* address comments

* update dockerfile (#3819)

* fix dockerfile

* fix(security): pentest remediation — condition escaping, SSRF hardening, ReDoS protection (#3820)

* fix(executor): escape newline characters in condition expression strings

Unescaped newline/carriage-return characters in resolved string values
cause unterminated string literals in generated JS, crashing condition
evaluation with a SyntaxError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): prevent ReDoS in guardrails regex validation

Add safe-regex2 to reject catastrophic backtracking patterns before
execution and cap input length at 10k characters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): SSRF localhost hardening and regex DoS protection

Block localhost/loopback URLs in hosted environments using isHosted flag
instead of allowHttp. Add safe-regex2 validation and input length limits
to regex guardrails to prevent catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): validate regex syntax before safety check

Move new RegExp() before safe() so invalid patterns get a proper syntax
error instead of a misleading "catastrophic backtracking" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): address PR review feedback

- Hoist isLocalhost && isHosted guard to single early-return before
  protocol checks, removing redundant duplicate block
- Move regex syntax validation (new RegExp) before safe-regex2 check
  so invalid patterns get proper syntax error instead of misleading
  "catastrophic backtracking" message

* fix(security): remove input length cap from regex validation

The 10k character cap would block legitimate guardrail checks on long
LLM outputs. Input length doesn't affect ReDoS risk — the safe-regex2
pattern check already prevents catastrophic backtracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tests): mock isHosted in input-validation and function-execute tests

Tests that assert self-hosted localhost behavior need isHosted=false,
which is not guaranteed in CI where NEXT_PUBLIC_APP_URL is set to the
hosted domain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* improvement(worker): configuration defaults (#3821)

* improvement(worker): configuration defaults

* update readmes

* realtime curl import

* improvement(tour): remove auto-start, only trigger on explicit user action (#3823)

* fix(mcp): use correct modal for creating workflow MCP servers in deploy (#3822)

* fix(mcp): use correct modal for creating workflow MCP servers in deploy

* fix(mcp): show workflows field during loading and when empty

* mock course

* fix(db): use bigint for token counter columns in user_stats (#3755)

* mock course

* updates

* updated X handle for emir

* cleanup: audit and clean academy implementation

* fix(academy): add label to ValidationRule, fix quiz gating, simplify getRuleMessage

* cleanup: remove unnecessary comments across academy files

* refactor(academy): simplify abstractions and fix perf issues

* perf(academy): convert course detail page to server component with client island

* fix(academy): null-safe canAdvance, render exercise instructions, remove stale comments

* fix(academy): remove orphaned migration, fix getCourseById, clean up comments

- Delete 0181_academy_certificate.sql (orphaned duplicate not in journal)
- Add getCourseById() to content/index.ts; use it in certificates API
  (was using getCourse which searches by slug, not stable id)
- Remove JSX comments from catalog page
- Remove redundant `passed` recomputation in LessonQuiz

* chore(db): regenerate academy_certificate migration with drizzle-kit

* chore: include blog mdx and components changes

* fix(blog): correct cn import path

* fix(academy): constrain progress bar to max-w-3xl with proper padding

* feat(academy): show back-to-course button on first lesson

* fix(academy): force dark theme on all /academy routes

* content(academy): rewrite sim-foundations course with full 6-module curriculum

* fix(academy): correct edge handles, quiz explanation, and starter mock outputs

- Fix Exercise 2 initial edge handles: 'starter-1-source'/'agent-1-target' → 'source'/'target' (React Flow actual IDs)
- Fix M1-L4 Q4 quiz explanation: remove non-existent Ctrl/Cmd+D and Alt+drag shortcuts
- Add starter mock output to all exercises so run animation shows feedback on the first block

* refine(academy): fix inaccurate content and improve exercise clarity

- Fix Exercise 3: replace hardcoded <agent-1.content> (invalid UUID-based ref) with reference picker instructions
- Fix M4 Quiz Q5: Loop block (subflow container) is correct answer, not the Workflow block
- Fix M4 Quiz Q4: clarify fan-out vs Parallel block distinction in explanation
- Fix M4-L2 video description: accurately describe Loop and Parallel subflow blocks
- Fix M2 Quiz Q3: make response format question conceptual rather than syntax-specific
- Improve Exercise 4 branching instructions: clarify top=true / bottom=false output handles
- Improve Final Project instructions: step-by-step numbered flow

* fix(academy): remove double border on quiz question cards

* fix(academy): single scroll container on lesson pages — remove nested flex scroll

* fix(academy): remove min-h-screen from root layout — fixes double scrollbar on lesson pages

* fix(academy): use fixed inset-0 on lesson page to eliminate document-level scrollbar

* fix(academy): replace sr-only radio/checkbox inputs with buttons to prevent scroll-on-focus; restore layout min-h-screen

* improvement(academy): polish, security hardening, and certificate claim UI

- Replace raw localStorage with BrowserStorage utility in local-progress
- Pre-compute slug/id Maps in content/index for O(1) course lookups
- Move blockMap construction into edge_exists branch only in validation
- Extract navBtnClass constant and MetaRow/formatDate helpers in UI
- Add rate limiting, server-side completion verification, audit logging, and nanoid cert numbers to certificate issuance endpoint
- Add useIssueCertificate mutation hook with completedLessonIds
- Wire certificate claim UI into CourseProgress: sign-in prompt, claim button with loading state, and post-issuance view with link to certificate page
- Fix lesson page scroll container and quiz scroll-on-focus bug

* fix(academy): validate condition branch handles in edge_exists rules

- Add sourceHandle field to edge_exists ValidationRule type
- Check sourceHandle in validation.ts when specified
- Require both condition-if and condition-else branches to be connected in the branching and final project exercises

* fix(academy): address PR review — isHosted regression, stuck isExecuting, revoked cert 500, certificate SSR

- Restore env-var-based isHosted check (was hardcoded true, breaking self-hosted deployments)
- Fix isExecuting stuck at true when mock run fails validation — set isMockRunningRef immediately and reset both flags on early exit
- Fix revoked/expired certificate causing 500 — any existing record (not just active) now returns 409 instead of falling through to INSERT
- Convert certificate verification page from client component to server component — direct DB fetch, notFound() on missing cert, generateMetadata for SEO/social previews

* fix(auth): restore hybrid.ts from staging to fix CI type error

* fix(academy): mark video lessons complete on visit and fix sign-in path

* fix(academy): replace useEffect+setState with lazy useState initializer in CourseProgress

* fix(academy): reset exerciseComplete on lesson navigation, remove unused useAcademyCertificate hook

* fix(academy): useState for slug-change reset, cache() for cert page, handleMockRunRef for stale closure

* fix(academy): replace shadcn theme vars with explicit hex in LessonVideo fallback

* fix(academy): reset completedRef on exercise change, conditional verified badge, multi-select empty guard

* fix(academy): type safety fixes — null metadata fallbacks, returning() guard, exhaustive union, empty catch

* fix(academy): reset ExerciseView completed banner on nav; fix CourseProgress hydration mismatch

* fix(lightbox): guard effect body with isOpen to prevent spurious overflow reset

* fix(academy): reset LessonQuiz state on lesson change to prevent stale answers persisting

* fix(academy): course not-found metadata title; try-finally guard in mock run loop

* fix(academy): type safety, cert persistence, regex guard, mixed-lesson video, shorts support

- Derive AcademyCertificate from db $inferSelect to prevent schema drift
- Add useCourseCertificate query hook; GET /api/academy/certificates now accepts courseId for authenticated lookup
- Use useCourseCertificate in CourseProgress so certificate state survives page refresh
- Guard new RegExp(valuePattern) in validation.ts with try/catch; log warn on invalid pattern
- Add logger.warn for custom validation rules so content authors are alerted
- Add YouTube Shorts URL support to LessonVideo (youtube.com/shorts/VIDEO_ID)
- Fix mixed-lesson video gap: render videoUrl above quiz when mixed has quiz but no exercise
- Add academy-scoped not-found.tsx with link back to /academy

* fix(academy): reset hintIndex when exercise changes

* chore: remove ban-spam-accounts script (wrong branch)

* fix(academy): enforce availableBlocks in toolbar; fix mixed exercise+quiz rendering

- Add useSandboxBlockConstraints context; SandboxCanvasProvider provides exerciseConfig.availableBlocks so the toolbar only shows permitted block types. Empty array hides all blocks (configure-only exercises); non-null array restricts to listed types; triggers always hidden in sandbox.
- Fix mixed lesson with both exerciseConfig and quizConfig: exercise renders first, quiz reveals after exercise completes (sequential pedagogy). canAdvance now requires both exerciseComplete && quizComplete when both are present.

* chore(academy): remove extraneous inline comments

* fix(academy): blank mixed lesson, quiz canAdvance flag, empty-array valueNotEmpty

* prep for merge

* chore(db): regenerate academy certificate migration after staging merge

* fix(academy): disable auto-connect in sandbox mode

* fix(academy): render video in mixed lesson with no exercise or quiz

* fix(academy): mark mixed video-only lessons complete; handle cert insert race

* fix(canvas): add sandbox and embedded to nodes useMemo deps

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Theodore Li <teddy@zenobiapay.com>
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