Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env

This file was deleted.

21 changes: 19 additions & 2 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { FollowupPanel } from '@/components/followup-panel'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents'
import { writer } from '@/lib/agents/writer'
import { saveChat, getSystemPrompt, generateReportContext } from '@/lib/actions/chat'
import { enqueueJob } from '@/lib/actions/jobs'
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'
import { send } from '@vercel/queue'
import { Chat, AIMessage } from '@/lib/types'
import { UserMessage } from '@/components/user-message'
import { BotMessage } from '@/components/message'
Expand Down Expand Up @@ -57,9 +60,23 @@ async function submit(formData?: FormData, skip?: boolean) {
}
try {
const messages = JSON.parse(messagesString) as AIMessage[];
return await generateReportContext(messages);
const userId = await getCurrentUserIdOnServer() || 'anonymous';

// Enqueue job for asynchronous processing
const jobId = await enqueueJob(userId, 'generate_report_context', { messages });

// Send to Vercel Queue
try {
await send('report-generation', { jobId });
} catch (queueError) {
console.error('Failed to send to Vercel Queue:', queueError);
// We continue because the worker might still poll if it's running,
// but ideally Vercel Queue handles the trigger.
}

return { jobId, status: 'processing' };
} catch (e) {
console.error('Failed to parse messages for report context:', e);
console.error('Failed to enqueue report context job:', e);
return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' };
}
}
Expand Down
43 changes: 43 additions & 0 deletions app/api/queue/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { handleCallback } from '@vercel/queue';
import { generateReportContext } from '@/lib/actions/chat';
import { updateJob, getJob } from '@/lib/actions/jobs';
import { NextRequest } from 'next/server';

const queueHandler = handleCallback(async (payload: any) => {
const { jobId } = payload;
if (!jobId) {
console.error('No jobId in queue payload');
return;
}
Comment on lines +8 to +11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Early returns prevent Vercel Queue from retrying failures.

Lines 9 and 16 return early on missing jobId or job not found. Per Vercel Queue documentation, the callback should throw an error to trigger retries. Returning without throwing signals success, and the message is removed from the queue.

If jobId is missing or the job is not found, these are likely transient or poison-message issues. For poison messages, you should throw to let Vercel Queue apply retry/DLQ logic. For transient DB failures (job not found due to replication lag), throwing enables retry.

🔁 Recommended fix: throw instead of return
   const { jobId } = payload;
   if (!jobId) {
     console.error('No jobId in queue payload');
-    return;
+    throw new Error('No jobId in queue payload');
   }

   // ...
   const job = await getJob(jobId);
   if (!job) {
     console.error(`Job ${jobId} not found`);
-    return;
+    throw new Error(`Job ${jobId} not found`);
   }

Also applies to: 14-17

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/queue/route.ts` around lines 7 - 10, Replace early returns with
thrown errors so Vercel Queue can retry: where the code checks for missing jobId
(the if (!jobId) block) and where it checks for a missing job record (the "job
not found" branch), change the return to throw a new Error including a clear
message and any relevant identifiers (e.g., jobId) and still log details via
console.error before throwing; ensure both checks throw so Vercel Queue can
apply retry/DLQ logic instead of treating the message as successfully handled.


try {
const job = await getJob(jobId);
if (!job) {
console.error(`Job ${jobId} not found`);
return;
}

await updateJob(jobId, { status: 'processing' });

const { messages } = job.payload as { messages: any[] };
const result = await generateReportContext(messages);

await updateJob(jobId, {
status: 'completed',
result,
updatedAt: new Date(),
});
console.log(`Job ${jobId} completed via Vercel Queue`);
} catch (error) {
console.error(`Job ${jobId} failed in Vercel Queue:`, error);
await updateJob(jobId, {
status: 'failed',
error: error instanceof Error ? error.message : String(error),
updatedAt: new Date(),
});
}
});
Comment on lines +1 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify route structure and Redis integration

# Check for the expected route path
fd -t f 'route.ts' app/api/queues/

# Search for Upstash Redis setup and usage
rg -n 'Redis|UPSTASH' --type ts --type env -C2

# Check vercel.json for queue configuration
cat vercel.json 2>/dev/null || echo "vercel.json not found"

Repository: QueueLab/QCX

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the queue callback route implementation(s)
echo "== handleCallback routes =="
rg -n "handleCallback" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

# 2) Inspect app/api tree relevant to queue/generate-report
echo
echo "== List api route directories (shallow) =="
ls -la app/api 2>/dev/null || true
find app/api -maxdepth 3 -type f -name "route.ts" 2>/dev/null | sed 's|^\./||' | sort || true

# 3) Read the provided route file if it exists
echo
echo "== app/api/queue/route.ts (first 120 lines) =="
if [ -f "app/api/queue/route.ts" ]; then
  sed -n '1,120p' app/api/queue/route.ts
else
  echo "MISSING: app/api/queue/route.ts"
fi

# 4) Inspect jobs helpers for updatedAt behavior
echo
echo "== updateJob/getJob implementations =="
rg -n "function updateJob|export (async )?function updateJob|updateJob\\(" app/lib/actions/jobs.ts app/lib/actions/jobs.* app/lib/actions -S 2>/dev/null || true
for f in $(fd -t f "jobs.ts" app/lib/actions 2>/dev/null || true); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

# 5) Search for Upstash Redis usage and env vars
echo
echo "== Upstash/Redis usage =="
rg -n "UPSTASH|Upstash|upstash|Redis" -S --glob "**/*.ts" --glob "**/*.js" --glob "**/*.tsx" --glob "**/*.env*" --glob "vercel.json" . || true

# 6) Check queue-related vercel config (if present)
echo
echo "== vercel.json (contents if exists) =="
if [ -f "vercel.json" ]; then
  cat vercel.json
else
  echo "MISSING: vercel.json"
fi

# 7) Check other queue/route paths possibly matching the PR summary expectation
echo
echo "== Look for generate-report routes =="
find app/api -type f -name "route.ts" 2>/dev/null | xargs -r -n 1 basename | head -n 200 || true
rg -n "generate-report|generateReport|report" app/api -S --glob "**/*.ts" --glob "**/*.tsx" || true

Repository: QueueLab/QCX

Length of output: 2420


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate jobs helper files
echo "== Locate jobs helper(s) =="
fd -t f -i "*jobs*.ts" app/lib || true
fd -t f -i "jobs.ts" app/lib || true
fd -t f -i "*job*.ts" app/lib/actions || true

# Read likely jobs files
for f in $(fd -t f -i "*jobs*.ts" app/lib 2>/dev/null); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

# Search for upstash/redis clients (beyond env mentions)
echo
echo "== Redis/Upstash client code usage =="
rg -n "(upstash|UPSTASH|`@upstash`\/redis|Redis|createClient|redis\.)" -S --glob "**/*.ts" --glob "**/*.tsx" app lib src . || true

# Ensure we understand queue callback retry behavior usage in codebase
echo
echo "== Other `@vercel/queue` usage patterns =="
rg -n "`@vercel/queue`|handleCallback" -S --glob "**/*.ts" . || true

Repository: QueueLab/QCX

Length of output: 791


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find updateJob/getJob definitions =="
rg -n "updateJob\\b|getJob\\b" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Find generateReportContext definition =="
rg -n "generateReportContext\\b" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== List files named jobs.ts / actions/jobs =="
fd -t f -i "jobs.ts" . || true
fd -t f -i "jobs" . || true

echo
echo "== Inspect any found jobs-related file(s) (first 200 lines) =="
# Take a limited set of likely matches to avoid huge output
for f in $(rg -l "export (async )?function updateJob|export (async )?function getJob|updateJob\\(|getJob\\(" -S --glob "**/*.ts" . | head -n 20); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

echo
echo "== Redis/Upstash code usage (broader search) =="
rg -n "(upstash|UPSTASH|`@upstash/redis`|upstash_redis|redis)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Find vercel/queue config docs/examples usage =="
rg -n "handleCallback|`@vercel/queue`" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Locate any queue payload fields or TTL write logic =="
rg -n "(TTL|ttl|expire|expiration|setex|hset|write.*redis|redis.*set)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

Repository: QueueLab/QCX

Length of output: 4958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generateReportContext implementation =="
sed -n '1,200p' lib/actions/chat.ts
echo
echo "== generateReportContext (tail if longer) =="
sed -n '200,400p' lib/actions/chat.ts 2>/dev/null || true

Repository: QueueLab/QCX

Length of output: 7912


🌐 Web query:

@vercel/queue handleCallback retry behavior throw vs return

💡 Result:

In @vercel/queue, the behavior of handleCallback regarding exceptions and return values is as follows: Throwing an Error: When your handler function throws an error, the message is not acknowledged and is automatically scheduled for redelivery [1][2]. This triggers the retry process, where the message will be delivered again after a delay—either the default interval or a custom delay if one is defined in the retry configuration [2][3]. Returning: Messages are automatically acknowledged (marked as successfully processed) when your handler function completes execution without throwing an error [1][4]. Custom Retry Configuration: You can control this behavior using the retry option within the handleCallback configuration object [1][2]. This option accepts a callback that receives the error and metadata [2][3]. Based on the return value of this retry callback, you can customize the outcome: - { afterSeconds: number }: Reschedules the message for redelivery after the specified number of seconds [1][3]. - { acknowledge: true }: Acknowledges the message immediately, effectively stopping any further retries [1][2]. - undefined: Allows the error to propagate normally, causing the system to use the default retry behavior (redelivery after the default interval) [2][3]. It is important to ensure your handler is idempotent, as retries will cause your code to execute multiple times if a previous attempt failed [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for enqueueJob/send usage =="
rg -n "enqueueJob\\b|`@vercel/queue`\\b|send\\b\\(" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Inspect app/actions.tsx around Vercel Queue send =="
if [ -f "app/actions.tsx" ]; then
  nl -ba app/actions.tsx | sed -n '1,120p'
  nl -ba app/actions.tsx | sed -n '120,240p'
else
  echo "MISSING: app/actions.tsx"
fi

echo
echo "== Search for generate-report / queues route strings =="
rg -n "(generate-report|generateReport|/api/queues|api/queues|queues/)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Inspect jobs table schema =="
rg -n "export const jobs\\b|jobs\\b.*schema|updatedAt|result|error" -S --glob "**/*.ts" --glob "**/*.tsx" lib/app src . || true
# Print jobs schema file if we can locate it
for f in $(rg -l "export const jobs\\b" . || true); do
  echo
  echo "---- $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

Repository: QueueLab/QCX

Length of output: 699


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect app/actions.tsx around Vercel Queue send =="
if [ -f "app/actions.tsx" ]; then
  sed -n '1,140p' app/actions.tsx
else
  echo "MISSING: app/actions.tsx"
fi

echo
echo "== Inspect app/actions.tsx near enqueueJob/send call =="
if [ -f "app/actions.tsx" ]; then
  rg -n "enqueueJob|send\\(" app/actions.tsx
fi

echo
echo "== Search for generate-report / queues route strings =="
rg -n "(generate-report|generateReport|/api/queues|api/queues|queues/)" -S --glob "**/*.ts" --glob "**/*.tsx" . || true

echo
echo "== Locate jobs table schema (drizzle) =="
rg -n "jobs\\s*=\\s*|export const jobs\\b|updatedAt\\b|result\\b|error\\b" -S --glob "**/*.ts" --glob "**/*.tsx" lib . || true

echo
echo "== Print any file containing 'export const jobs' =="
for f in $(rg -l "export const jobs\\b" lib . 2>/dev/null || true); do
  echo
  echo "---- $f (first 220 lines) ----"
  sed -n '1,220p' "$f"
done

Repository: QueueLab/QCX

Length of output: 50368


Critical: Vercel Queue worker swallows failures, so jobs won’t retry

  • In app/api/queue/route.ts, the catch block updates the job to failed but does not re-throw; handleCallback will treat the handler as successful and acknowledge the message, preventing Vercel Queue retries. Re-throw the error after updateJob(...) (and only return for non-retriable cases like missing jobId/unknown job).
  • const { messages } = job.payload as { messages: any[] } has no runtime validation; add guards that job.payload exists and messages is an array before calling generateReportContext.
  • Remove redundant updatedAt: new Date() from updateJob(...) calls—lib/actions/jobs.ts already sets updatedAt internally.
  • Optional: add a timeout/abort around generateReportContext(messages) to avoid long-running/hung queue executions.

Also, the earlier “wrong route path” and “Upstash Redis storage” assertions don’t match this codebase: the producer uses send('report-generation', { jobId }) and the consumer is app/api/queue/route.ts, and job results are persisted to the Postgres jobs table via lib/actions/jobs.ts (no Upstash/Redis client usage found).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/queue/route.ts` around lines 1 - 38, The POST handler wrapped by
handleCallback should (1) re-throw the caught error after calling updateJob so
Vercel Queue can retry — modify the catch in the POST handler to await
updateJob(... status: 'failed' ...) and then throw the error instead of
returning; (2) validate runtime payload shape before processing by checking that
job.payload exists and that messages is an Array (e.g., const messages =
job.payload?.messages; if (!Array.isArray(messages)) handle as non-retriable
error or throw); (3) remove redundant updatedAt: new Date() from the
updateJob(...) calls (updateJob sets updatedAt internally) in both the
'completed' and 'failed' updates; and (4) optionally wrap
generateReportContext(messages) with a cancellable timeout/AbortSignal to guard
against long-running executions — locate these changes in the POST handler in
app/api/queue/route.ts referencing functions handleCallback, getJob, updateJob,
and generateReportContext.


export async function POST(request: NextRequest) {
return queueHandler(request);
}
39 changes: 39 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 29 additions & 1 deletion components/download-report-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AI } from '@/app/actions'
import { useActions } from 'ai/rsc'
import { toast } from 'sonner'
import { ReportTemplate } from './report-template'
import { getJobStatus } from '@/lib/actions/jobs-client'
import { createPortal } from 'react-dom'

export const DownloadReportButton = () => {
Expand Down Expand Up @@ -57,7 +58,34 @@ export const DownloadReportButton = () => {
formData.append('action', 'generate_report_context');
formData.append('messages', JSON.stringify(aiState.messages));

const { title, summary } = await (actions as any).submit(formData);
const { jobId } = await (actions as any).submit(formData);

if (!jobId) {
throw new Error('Failed to start report generation job');
}

// Polling for job completion
let jobResult = null;
let attempts = 0;
const maxAttempts = 60; // 60 seconds

while (attempts < maxAttempts) {
const { status, result, error } = await getJobStatus(jobId);
if (status === 'completed') {
jobResult = result;
break;
} else if (status === 'failed') {
throw new Error(error || 'Job failed');
}
attempts++;
await new Promise(resolve => setTimeout(resolve, 1000));
}

if (!jobResult) {
throw new Error('Report generation timed out');
}

const { title, summary } = jobResult as { title: string, summary: string };
Comment on lines +61 to +88

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Polling implementation has multiple critical issues.

  1. Line 61: Unsafe destructuring assumes response shape.
    The submit action can return different shapes: { jobId, status } on success or { title, summary } on error (see app/actions.tsx lines 77 vs 80). Destructuring { jobId } without validating the response will produce undefined on the error path, causing the check on line 63 to throw a misleading error.

    Add a type guard:

    const response = await (actions as any).submit(formData);
    if (!('jobId' in response)) {
      throw new Error('Failed to start report generation');
    }
    const { jobId } = response;
  2. Lines 72-82: Polling server actions in a tight loop is inefficient and blocks the client.

    • Each getJobStatus(jobId) call is a server action that makes a network round-trip. Calling it 60 times at 1-second intervals is expensive and creates unnecessary load.
    • The await new Promise(resolve => setTimeout(resolve, 1000)) on line 81 blocks the client's render thread during each 1s sleep (though React 19 handles this better with concurrent rendering, it's still suboptimal).
    • Consider using exponential backoff (e.g., 500ms → 1s → 2s → 4s) to reduce server load and improve UX.
    • Consider moving polling to a non-blocking pattern (e.g., React Query or SWR with refetch intervals).
  3. Line 88: Unsafe type cast without validation.
    jobResult as { title: string, summary: string } assumes the shape without checking. If the result shape is different (e.g., missing summary), the code will silently use undefined.

    Add validation:

    if (!jobResult || typeof jobResult !== 'object' || !('title' in jobResult)) {
      throw new Error('Invalid job result format');
    }
    const { title, summary } = jobResult as { title: string; summary: string };
  4. No cleanup on component unmount.
    If the user navigates away during polling, the loop continues running in the background until timeout. Add cleanup with useEffect and an abort signal or flag to stop polling when the component unmounts.

  5. Magic number 60 without explanation.
    Line 70: const maxAttempts = 60; // 60 seconds assumes 1-second intervals, but if intervals change, the comment becomes misleading. Use a named constant or calculate from timeout duration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/download-report-button.tsx` around lines 61 - 88, The polling
logic in DownloadReportButton incorrectly destructures the submit response,
polls inefficiently, assumes jobResult shape, lacks unmount cleanup, and uses a
magic attempts constant; fix by first assigning the submit result to a variable
and type-guarding that it contains jobId before destructuring (replace direct
`const { jobId } = await (actions as any).submit(...)` with a response check for
'jobId'), replace the fixed 1s tight loop using getJobStatus(jobId) with an
exponential-backoff retry strategy (e.g., baseDelay doubling on each attempt and
a total timeout constant instead of maxAttempts) to reduce server load, validate
the final jobResult object shape before casting (ensure it has required title
and summary fields and throw if invalid), add component unmount cleanup (use an
AbortSignal or a mounted flag in useEffect to stop further polling and cancel
pending timers/requests), and replace the magic `maxAttempts` number with a
named timeout/duration constant computed from baseDelay and maxRetries.


const finalTitle = title || 'QCX Intelligence Analysis'
setReportTitle(finalTitle)
Expand Down
7 changes: 7 additions & 0 deletions lib/actions/jobs-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use server'

import { getJobStatus as getJobStatusServer } from './jobs'

export async function getJobStatus(jobId: string) {
return await getJobStatusServer(jobId)
}
Comment on lines +1 to +7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: This file should have been removed or replaced.

Per the PR summary and bot migration plan:

"Replace lib/actions/jobs-client.ts with a Redis-backed getJobStatus(jobId) that reads report-job:{messageId} from Upstash Redis and returns processing/completed/failed states."

The current implementation is an unnecessary wrapper that imports getJobStatus from ./jobs, aliases it, and re-exports it without adding any value. If the DB-backed approach is kept, this indirection should be removed; if migrating to Vercel Queue + Redis, this should be replaced with a Redis-based implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs-client.ts` around lines 1 - 7, Replace the trivial wrapper
getJobStatus in lib/actions/jobs-client.ts: remove the import/alias of
getJobStatusServer and either delete this file and update callers to call the
real getJobStatus in ./jobs, or implement the required Redis-backed
getJobStatus(jobId) that reads the Upstash key report-job:{jobId}, parses the
stored value and returns the normalized state
("processing"|"completed"|"failed"); if using Vercel Queue + Redis, implement
the same Redis lookup inside getJobStatus and map stored payloads to those
states. Ensure you reference and update any callers that currently import
getJobStatus from lib/actions/jobs-client.ts to use the new
location/implementation.

54 changes: 54 additions & 0 deletions lib/actions/jobs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use server'

import { db } from '@/lib/db'
import { jobs } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { revalidatePath } from 'next/cache'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove unused import.

revalidatePath is imported but never used in this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` at line 6, Remove the unused import revalidatePath from
the module; update lib/actions/jobs.ts by deleting the "import { revalidatePath
} from 'next/cache'" statement (ensure no other code relies on revalidatePath
and that imports in that file like any other named imports remain correctly
formatted).


export async function enqueueJob(userId: string, type: string, payload: any) {
try {
const result = await db.insert(jobs).values({
userId,
type,
payload,
status: 'pending',
}).returning({ id: jobs.id })

return result[0].id
} catch (error) {
console.error('Error enqueuing job:', error)
throw error
}
}

export async function getJob(jobId: string) {
try {
const result = await db.select().from(jobs).where(eq(jobs.id, jobId)).limit(1)
return result[0] || null
} catch (error) {
console.error('Error getting job:', error)
return null
}
}
Comment on lines +8 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Inconsistent error handling across CRUD functions.

enqueueJob (line 20) and updateJob (line 41) rethrow errors, while getJob (line 30) catches and returns null. This inconsistency forces callers to handle two different failure modes (exceptions vs null checks) for similar operations.

Consider a unified strategy: either propagate all errors or return Result<T, Error> types consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` around lines 8 - 32, Unify error handling across job
CRUD functions by making all functions propagate errors instead of swallowing
them; specifically, update getJob (and any other read helpers like updateJob) to
either remove the try/catch or rethrow after logging so it behaves like
enqueueJob (which currently throws). Locate the getJob function and change its
error path to call console.error('Error getting job:', error) and then throw
error (or remove the catch entirely), ensuring callers receive exceptions
consistently rather than sometimes getting null.


export async function updateJob(jobId: string, updates: Partial<typeof jobs.$inferInsert>) {
try {
await db.update(jobs)
.set({ ...updates, updatedAt: new Date() })
.where(eq(jobs.id, jobId))
} catch (error) {
console.error('Error updating job:', error)
throw error
}
}
Comment on lines +34 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Type mismatch: updatedAt set manually but included in inferred insert type.

Line 37 manually sets updatedAt: new Date(), but the updates parameter is typed as Partial<typeof jobs.$inferInsert>. The $inferInsert type includes updatedAt (which has a default), so callers could pass it in updates and it would be overwritten. Additionally, if updates contains fields not valid for updates (e.g., id), the type doesn't prevent it.

Prefer a dedicated update shape type or explicitly omit non-updatable fields:

type JobUpdate = Partial<Pick<typeof jobs.$inferInsert, 'status' | 'result' | 'error' | 'payload'>>

export async function updateJob(jobId: string, updates: JobUpdate) {
  // ...
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` around lines 34 - 43, The updateJob function currently
types updates as Partial<typeof jobs.$inferInsert> which allows callers to pass
non-updatable fields (e.g., id) or updatedAt which you then overwrite; create a
dedicated update shape (e.g., type JobUpdate = Partial<Pick<typeof
jobs.$inferInsert, 'status' | 'result' | 'error' | 'payload'>> or use
Omit<typeof jobs.$inferInsert, 'id' | 'createdAt' | 'updatedAt'> wrapped in
Partial) and change the updateJob signature to updateJob(jobId: string, updates:
JobUpdate), keep the server-side updatedAt: new Date() assignment, and
optionally assert or throw if updates contains forbidden keys before calling
db.update so callers cannot override id/updatedAt.


export async function getJobStatus(jobId: string) {
'use server'
Comment on lines +45 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove redundant 'use server' directive.

Line 1 already declares 'use server' for the entire module, making this function-level directive redundant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/jobs.ts` around lines 45 - 46, Remove the redundant
function-level "'use server'" directive inside getJobStatus: open the
getJobStatus function (export async function getJobStatus) and delete the "'use
server'" string so the module-level directive only remains; no behavioral
changes otherwise.

const job = await getJob(jobId)
if (!job) return { status: 'not_found' }
return {
status: job.status,
result: job.result,
error: job.error
}
}
20 changes: 20 additions & 0 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ export const calendarNotes = pgTable('calendar_notes', {
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});

export const jobs = pgTable('jobs', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: text('type').notNull(),
status: text('status').notNull().default('pending'),
payload: jsonb('payload'),
result: jsonb('result'),
error: text('error'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});
Comment on lines +105 to +115

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for Upstash Redis usage and migration to drop jobs table

# Search for Upstash Redis imports/usage
rg -n '`@upstash/redis`' --type ts -C2

# Search for migration files that drop jobs table
fd -e sql -e ts 'migrat' --exec rg -l 'DROP TABLE.*jobs'

# Verify if jobs table is referenced in queue handler
rg -n "db.*jobs|from.*jobs|jobs\\..*" app/api/queue/route.ts app/api/queues/ -A2

Repository: QueueLab/QCX

Length of output: 323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Upstash Redis usage
echo "=== Upstash Redis imports/usage ==="
rg -n "`@upstash/redis`" --type ts --type js -S . || true

# 2) Where migrations live, and whether any drop jobs table exists
echo
echo "=== Migration files mentioning jobs table drops ==="
# Try common migration dirs/patterns without assuming structure
for dir in ./.; do
  fd -t f -e sql -e ts -e js -e mjs -e cjs -e kys -e drizzle -e knex . \
    --hidden --exclude-dir node_modules --exclude-dir dist --exclude-dir build \
    | rg -n "migrat|migration|drizzle|knex|sequelize|schema" -S -g'*.{sql,ts,js,mjs,cjs}' \
    | head -n 2000 >/tmp/migration_candidates.txt || true

  if [ -s /tmp/migration_candidates.txt ]; then
    xargs -r rg -n --no-heading -S "DROP TABLE.*jobs|drop table.*jobs|DROP\\s+TABLE\\s+jobs|alter table.*jobs" \
      < /tmp/migration_candidates.txt || true
  else
    echo "No migration candidate files found by heuristic."
  fi
done

# 3) References to the jobs table/schema
echo
echo "=== References to jobs schema/table ==="
rg -n "lib/db/schema|schema\\.ts|\\bjobs\\b" --type ts --type js -S lib app || true

# 4) Check queue handlers for job persistence
echo
echo "=== Queue handler references to jobs/db ==="
rg -n "handleCallback|queue|jobs|updateJob|getJob|jobs\\." --type ts -S app lib || true

# 5) Show the exact schema.ts around the jobs definition for context
echo
echo "=== lib/db/schema.ts around jobs definition ==="
if [ -f "lib/db/schema.ts" ]; then
  nl -ba lib/db/schema.ts | sed -n '80,140p'
else
  echo "lib/db/schema.ts not found"
fi

Repository: QueueLab/QCX

Length of output: 4933


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search: `@upstash/redis` and upstash usage ==="
rg -n "`@upstash/redis`|`@upstash`|upstash" --type ts --type js --hidden -S . || true
echo
rg -n "UPSTASH|UPSTASH_REDIS|REDIS_URL|REDIS" --hidden -S . || true

echo
echo "=== Search: queue + DB jobs persistence ==="
rg -n "handleCallback|`@vercel/queue`|enqueueJob|updateJob|getJob" --type ts --type js -S app lib || true

echo
echo "=== Migration directories/files discovery ==="
# List likely migration-related files/dirs without assuming structure
rg -n "drizzle|migration" --hidden -S . --glob '!**/node_modules/**' --glob '!**/dist/**' || true

# Find candidate SQL/TS migration files by name (heuristic)
echo
echo "=== Candidate migration files (by filename) ==="
find . -maxdepth 6 -type f \
  \( -iname "*migration*" -o -iname "*migrations*" -o -iname "*drizzle*" -o -iname "*schema*" -o -iname "*ops*" \) \
  \( -iname "*.sql" -o -iname "*.ts" -o -iname "*.js" -o -iname "*.mjs" \) 2>/dev/null | head -n 200 || true

echo
echo "=== Search migrations for jobs table drop ==="
# Search SQL/TS/JS for drop/alter statements on jobs (heuristic)
rg -n "DROP\\s+TABLE\\s+jobs|drop\\s+table\\s+jobs|ALTER\\s+TABLE\\s+jobs|alter\\s+table\\s+jobs" --hidden -S . \
  --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/.next/**' || true

echo
echo "=== Show lib/db/schema.ts jobs section (lines ~100-140) ==="
if [ -f lib/db/schema.ts ]; then
  sed -n '95,145p' lib/db/schema.ts
else
  echo "lib/db/schema.ts not found"
fi

Repository: QueueLab/QCX

Length of output: 11180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Directory listing: drizzle/* ==="
ls -la drizzle 2>/dev/null || true
ls -la drizzle/migrations 2>/dev/null || true
find drizzle -maxdepth 2 -type f 2>/dev/null | head -n 200 || true

echo
echo "=== Search drizzle/migrations for jobs table changes ==="
rg -n "DROP\\s+TABLE\\s+jobs|drop\\s+table\\s+jobs|ALTER\\s+TABLE\\s+jobs|alter\\s+table\\s+jobs|jobs\\b" -S drizzle/migrations 2>/dev/null || true

echo
echo "=== Inspect lib/actions/jobs.ts ==="
if [ -f lib/actions/jobs.ts ]; then
  sed -n '1,120p' lib/actions/jobs.ts
else
  echo "lib/actions/jobs.ts not found"
fi

echo
echo "=== Inspect app/api/queue/route.ts ==="
if [ -f app/api/queue/route.ts ]; then
  sed -n '1,120p' app/api/queue/route.ts
else
  echo "app/api/queue/route.ts not found"
fi

echo
echo "=== Status values used in queue handler code ==="
rg -n "updateJob\\(.*status\\s*:\\s*'|status\\s*:\\s*'|status:\\s*\"|set\\(\"status\"|processing|pending|completed|failed|error" -S app lib

echo
echo "=== Queries filtering by jobs.userId or jobs.status ==="
rg -n "from\\(jobs\\)|\\.select\\(\\).*jobs|where\\(eq\\(jobs\\.(userId|status)" -S lib app

Repository: QueueLab/QCX

Length of output: 25508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Show drizzle/migrations/*.sql files headers and any jobs-related statements ==="
for f in drizzle/migrations/*.sql; do
  echo
  echo "--- $f ---"
  # Print lines around any mention of jobs
  rg -n "\\bjobs\\b" "$f" -n || true
  # Print full file if small-ish, else only relevant ranges
  lines=$(wc -l < "$f")
  echo "Lines: $lines"
  if [ "$lines" -le 200 ]; then
    cat "$f"
  else
    # best-effort: show sections near jobs keywords
    rg -n -C3 "\\bjobs\\b" "$f" || true
  fi
done

echo
echo "=== Search for DROP/ALTER jobs in the whole migrations folder ==="
rg -n "DROP\\s+TABLE\\s+jobs|drop\\s+table\\s+jobs|ALTER\\s+TABLE\\s+jobs|alter\\s+table\\s+jobs" drizzle/migrations || true

echo
echo "=== Check drizzle.config.ts and migrate.ts migrationsFolder setting ==="
sed -n '1,120p' drizzle.config.ts
sed -n '1,120p' lib/db/migrate.ts

Repository: QueueLab/QCX

Length of output: 5253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for indexes/constraints on jobs in lib/db/schema.ts ==="
rg -n "index\\(|unique\\(|check\\(|constraint\\(|enum\\(" lib/db/schema.ts -S || true

echo
echo "=== Show jobs table block again with nearby lines (for context) ==="
sed -n '90,140p' lib/db/schema.ts

Repository: QueueLab/QCX

Length of output: 2259


Critical: Queue worker still persists into Postgres jobs, but Drizzle migrations don’t create (or remove) that table, and there’s no real Upstash Redis usage.

  • app/api/queue/route.ts + lib/actions/jobs.ts update jobs.status/result/error, and lib/db/schema.ts defines the jobs table.
  • drizzle/migrations/*.sql only create chats/messages/users and calendar_notes—there’s no migration creating (or dropping) jobs, so this will break on a fresh DB.
  • No @upstash/redis integration exists in code (only README/env mentions), so the “Upstash Redis for result storage” PR objective isn’t reflected here.
  • status is unconstrained text (add enum/CHECK). updatedAt is explicitly set in updateJob(), so it does update during job state changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/db/schema.ts` around lines 105 - 115, The PR left a persistent Postgres
"jobs" table (export const jobs) without corresponding Drizzle migrations and
without real Upstash Redis integration; add a new migration SQL that
creates/drops the jobs table matching the schema in lib/db/schema.ts (including
id, user_id FK, payload/result/error JSONB, created_at/updated_at timestamps)
and add a CHECK or ENUM constraint for status (allowed values like
'pending','running','completed','failed'); update lib/actions/jobs.ts and
app/api/queue/route.ts to either (a) move result storage to Upstash Redis
(integrate `@upstash/redis` client and store result keys there, reading/writing
from Redis) or (b) keep results in Postgres but ensure migrations reflect that
decision, and ensure updateJob() correctly sets updatedAt (or rely on DB
trigger/default) so updatedAt changes on state updates.


// Relations
export const usersRelations = relations(users, ({ many }) => ({
chats: many(chats),
Expand All @@ -111,6 +123,14 @@ export const usersRelations = relations(users, ({ many }) => ({
systemPrompts: many(systemPrompts),
locations: many(locations),
visualizations: many(visualizations),
jobs: many(jobs),
}));
Comment on lines +126 to +127

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove relations for jobs table.

Since the jobs table should not exist (see previous comment), these ORM relations linking users ↔ jobs must also be removed.

Also applies to: 129-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/db/schema.ts` around lines 126 - 127, Remove the ORM relations that
reference the non-existent jobs table: delete the "jobs: many(jobs)" entry
inside the users schema/object and any corresponding inverse relation entries in
the jobs-related schema (e.g., properties using many(jobs) or a user/jobs
relation) as noted around the same block (lines 129–134). Also remove any
imports, type references, or variables named "jobs" used solely for those
relations and adjust related types or validators in the same schema file (e.g.,
the users model/schema) so there are no lingering references to the jobs table.


export const jobsRelations = relations(jobs, ({ one }) => ({
user: one(users, {
fields: [jobs.userId],
references: [users.id],
}),
}));

export const chatsRelations = relations(chats, ({ one, many }) => ({
Expand Down
Loading