feat: Implement Chatbot and Model Selection - #261
Conversation
* **billing:** Restrict bring-your-own-AI-key (BYOK) configurations to Solo-and-above. Free plans now exclusively use the platform-provided OpenRouter key for the AI assistant. * **assistant:** Ship the AI assistant to all plans. Added a lifetime turn-based quota (`FREE_PLAN_CHATBOT_TURN_LIMIT`) for Free-tier workspaces to manage costs while allowing a functional demo. * **budgeting:** Integrate assistant turn usage into the platform-paid budget ledger. Assistant turns are now metered against the same monthly dollar-cap as analysis runs to prevent bypasses. * **UI:** Add `ChatbotQuotaUpsellCard.vue` to transition users who have exhausted their free turns into the Solo plan. * **architecture:** Introduce `ai_usage_event` ledger to record non-analysis AI spend and clarify usage for billing enforcement.
|
🚅 Deployed to the reqcore-pr-261 environment in applirank
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds chatbot plan entitlements, catalogue and BYOK model selection, provider routing, credit metering, safe input handling, and chatbot retention, export, and erasure support. ChangesChatbot platform and entitlement flow
Chatbot safety and data lifecycle
Estimated code review effort: 5 (Critical) | ~90 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
tests/unit/ai-budget-ledgers.test.ts (1)
71-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a split-ledger monthly-cap test.
Lines 71-75 only prove that both ledger tables are queried. The current cap cases put the full $20 in
aiUsageEvent. An implementation that does not add the two totals can still pass.Set
analysisRunandaiUsageEventto $10 each. Assert thatassertPlatformBudget()rejects withscope: 'org_monthly'.Proposed test
+ it('blocks a Solo org when combined ledger spend reaches the monthly cap', async () => { + state.spend.set(analysisRun, 10_000_000) + state.spend.set(aiUsageEvent, 10_000_000) + + await expect(assertPlatformBudget('org-1', { feature: 'chatbot' })) + .rejects.toMatchObject({ scope: 'org_monthly' }) + })🤖 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 `@tests/unit/ai-budget-ledgers.test.ts` around lines 71 - 75, Update the test using analysisRun and aiUsageEvent so each ledger contributes $10, then assert that assertPlatformBudget('org-1', { feature: 'chatbot' }) rejects with scope 'org_monthly'. Keep the existing queriedTables assertions to retain coverage that both ledgers are read.server/utils/ai/budget.ts (1)
104-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winOne counter, two copies.
countChatbotTurnsis defined twice with identical bodies. The meter and the gate must always report the same number, and two private copies make that a manual guarantee.
server/utils/ai/budget.ts#L104-L125: exportcountChatbotTurnsnext to the already-exportedfreeChatbotTurnLimit.server/utils/billing/usage.ts#L61-L75: delete the local copy and import the exported helper from../ai/budget.🤖 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 `@server/utils/ai/budget.ts` around lines 104 - 125, Export the existing countChatbotTurns helper in server/utils/ai/budget.ts alongside freeChatbotTurnLimit. In server/utils/billing/usage.ts, remove the duplicate local countChatbotTurns implementation and import the exported helper from ../ai/budget so both the meter and gate use the same counter.server/utils/ai/resolveProvider.ts (2)
130-149: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueOptional: reduce repeated lookups on the platform paths.
canUsePlatformAiandgetPlatformAiOverriderun here, andresolvePlatformAiProviderConfigruns both again internally. One assistant turn can therefore issue several duplicate plan and override queries. Consider passing the already-loaded override and plan into the resolver, or dropping the pre-checks and relying on the resolver's own 422.🤖 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 `@server/utils/ai/resolveProvider.ts` around lines 130 - 149, Reduce duplicate platform lookups in resolveChatbotProvider by reusing the override and plan data already fetched by the platform-selection checks, passing them into resolvePlatformAiProviderConfig if supported; alternatively remove the pre-checks and rely on the resolver’s existing 422 behavior. Preserve the current platform-provider selection and billingMode outcomes.
166-175: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winNarrow the catch to the "no BYOK config" case.
loadAiConfigthrows a 422 when no config exists. It can also reject for other reasons, for example a database failure. This catch treats every rejection as "no BYOK config" and switches the turn to the platform key. A transient database error then silently moves spend onto the platform budget instead of surfacing an error.Check the status code before falling back.
♻️ Proposed narrowing
catch (err) { // No BYOK config. Fall back to the platform engine unless it's unavailable // (grandfathered org, no server key, or the org switched it off). + // Only a 422 means "no config"; anything else is a real failure. + if ((err as { statusCode?: number })?.statusCode !== 422) throw err if (!await canUsePlatformAi(orgId)) throw err🤖 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 `@server/utils/ai/resolveProvider.ts` around lines 166 - 175, In the catch surrounding loadAiConfig, only execute the platform fallback when the caught error has the expected 422 status for missing BYOK configuration; rethrow all other errors before calling canUsePlatformAi, getPlatformAiOverride, or resolvePlatformAiProviderConfig. Preserve the existing fallback behavior for the no-config case.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@app/components/PublicPricingSection.vue`:
- Line 150: Update the pricing copy in PublicPricingSection so it does not
interpolate the fallback FREE_PLAN_CHATBOT_TURN_LIMIT as a guaranteed allowance;
use non-numeric wording, or source the value from public runtime configuration
backed by AI_FREE_PLAN_CHATBOT_TURN_LIMIT.
In `@server/api/chatbot/chat.post.ts`:
- Around line 467-482: Update the usage-ledger flow around recordAiUsage so
failed turns with no answer are excluded from the chatbot turn budget: only call
recordAiUsage when finalUsage is present, while preserving recording for turns
that produced output and their existing cost/token fields. Keep
countChatbotTurns unchanged unless choosing the alternative contract of
filtering both queries to rows with reported tokens.
In `@server/database/migrations/0060_happy_wolfpack.sql`:
- Line 17: Update the migration around the is_default_chatbot column addition to
backfill existing platform rows conditionally from the existing BYOK default
state, preserving BYOK defaults instead of marking every platform row as true.
Apply the final true default and NOT NULL constraint only after this backfill,
so toPlatformAiConfigListRow() does not expose conflicting defaults.
In `@tests/unit/chatbot-plan-gate.test.ts`:
- Around line 126-131: Extend the grandfathered-organization coverage around
resolveChatbotProvider with preferId: '__platform__', and update
resolveChatbotProvider to reject explicit platform selection before resolving
the platform provider when canUsePlatformAi is false. Preserve the existing “No
AI config” rejection behavior and ensure the platform key is never used for
grandfathered plans.
---
Nitpick comments:
In `@server/utils/ai/budget.ts`:
- Around line 104-125: Export the existing countChatbotTurns helper in
server/utils/ai/budget.ts alongside freeChatbotTurnLimit. In
server/utils/billing/usage.ts, remove the duplicate local countChatbotTurns
implementation and import the exported helper from ../ai/budget so both the
meter and gate use the same counter.
In `@server/utils/ai/resolveProvider.ts`:
- Around line 130-149: Reduce duplicate platform lookups in
resolveChatbotProvider by reusing the override and plan data already fetched by
the platform-selection checks, passing them into resolvePlatformAiProviderConfig
if supported; alternatively remove the pre-checks and rely on the resolver’s
existing 422 behavior. Preserve the current platform-provider selection and
billingMode outcomes.
- Around line 166-175: In the catch surrounding loadAiConfig, only execute the
platform fallback when the caught error has the expected 422 status for missing
BYOK configuration; rethrow all other errors before calling canUsePlatformAi,
getPlatformAiOverride, or resolvePlatformAiProviderConfig. Preserve the existing
fallback behavior for the no-config case.
In `@tests/unit/ai-budget-ledgers.test.ts`:
- Around line 71-75: Update the test using analysisRun and aiUsageEvent so each
ledger contributes $10, then assert that assertPlatformBudget('org-1', {
feature: 'chatbot' }) rejects with scope 'org_monthly'. Keep the existing
queriedTables assertions to retain coverage that both ledgers are read.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f45ebbd5-c702-4691-8441-9af8b5fae882
📒 Files selected for processing (38)
CHANGELOG.mdapp/components/AppTopBar.vueapp/components/ChatbotQuotaUpsellCard.vueapp/components/FreePlanUpgradeMenu.vueapp/components/FreePlanUpsellCard.vueapp/components/PublicPricingSection.vueapp/composables/useBillingStatus.tsapp/composables/useChatbotQuota.tsapp/pages/dashboard/chatbot/[[id]].vueapp/pages/dashboard/settings/ai/index.vueapp/pages/dashboard/settings/ai/new.vueapp/pages/dashboard/settings/billing.vueserver/api/ai-config/[id].patch.tsserver/api/ai-config/index.get.tsserver/api/ai-config/index.post.tsserver/api/billing/status.get.tsserver/api/chatbot/chat.post.tsserver/api/chatbot/conversations/[id].get.tsserver/api/chatbot/conversations/[id].patch.tsserver/api/chatbot/conversations/index.get.tsserver/api/chatbot/conversations/index.post.tsserver/database/migrations/0060_happy_wolfpack.sqlserver/database/migrations/meta/0060_snapshot.jsonserver/database/migrations/meta/_journal.jsonserver/database/schema/app.tsserver/utils/ai/budget.tsserver/utils/ai/platformConfig.tsserver/utils/ai/resolveProvider.tsserver/utils/ai/usage.tsserver/utils/billing/plan.tsserver/utils/billing/usage.tsserver/utils/chatbotAccess.tsserver/utils/chatbotConversation.tsshared/billing.tsshared/feature-flags.tstests/unit/ai-budget-ledgers.test.tstests/unit/billing-plan-resolution.test.tstests/unit/chatbot-plan-gate.test.ts
| ); | ||
| --> statement-breakpoint | ||
| ALTER TABLE "chatbot_conversation" ADD COLUMN "use_platform_ai" boolean DEFAULT false NOT NULL;--> statement-breakpoint | ||
| ALTER TABLE "platform_ai_config" ADD COLUMN "is_default_chatbot" boolean DEFAULT true NOT NULL;--> statement-breakpoint |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Backfill the platform default conditionally.
Line 17 sets is_default_chatbot to true for every existing platform row. If an organization already has a BYOK chatbot default, toPlatformAiConfigListRow() returns both engines as defaults because the persisted platform value overrides the fallback.
Set the new column from the existing BYOK default state before applying the final default and NOT NULL constraint.
Proposed migration change
-ALTER TABLE "platform_ai_config" ADD COLUMN "is_default_chatbot" boolean DEFAULT true NOT NULL;
+ALTER TABLE "platform_ai_config" ADD COLUMN "is_default_chatbot" boolean;
+UPDATE "platform_ai_config" AS platform
+SET "is_default_chatbot" = NOT EXISTS (
+ SELECT 1
+ FROM "ai_config" AS byok
+ WHERE byok."organization_id" = platform."organization_id"
+ AND byok."is_default_chatbot" = true
+);
+ALTER TABLE "platform_ai_config"
+ ALTER COLUMN "is_default_chatbot" SET DEFAULT true,
+ ALTER COLUMN "is_default_chatbot" SET NOT NULL;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ALTER TABLE "platform_ai_config" ADD COLUMN "is_default_chatbot" boolean DEFAULT true NOT NULL;--> statement-breakpoint | |
| ALTER TABLE "platform_ai_config" ADD COLUMN "is_default_chatbot" boolean; | |
| UPDATE "platform_ai_config" AS platform | |
| SET "is_default_chatbot" = NOT EXISTS ( | |
| SELECT 1 | |
| FROM "ai_config" AS byok | |
| WHERE byok."organization_id" = platform."organization_id" | |
| AND byok."is_default_chatbot" = true | |
| ); | |
| ALTER TABLE "platform_ai_config" | |
| ALTER COLUMN "is_default_chatbot" SET DEFAULT true, | |
| ALTER COLUMN "is_default_chatbot" SET NOT NULL; |
🤖 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 `@server/database/migrations/0060_happy_wolfpack.sql` at line 17, Update the
migration around the is_default_chatbot column addition to backfill existing
platform rows conditionally from the existing BYOK default state, preserving
BYOK defaults instead of marking every platform row as true. Apply the final
true default and NOT NULL constraint only after this backfill, so
toPlatformAiConfigListRow() does not expose conflicting defaults.
| it('never spends the platform key for a grandfathered org', async () => { | ||
| // Their free tier is explicitly BYOK-only — a missing config must surface as | ||
| // the "configure a provider" error, not a silent platform-paid turn. | ||
| state.plan = 'grandfathered' | ||
| await expect(resolveChatbotProvider('org-1')).rejects.toThrow('No AI config') | ||
| }) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Block explicit platform selection for grandfathered organizations.
resolveChatbotProvider resolves preferId: '__platform__' before it checks canUsePlatformAi. A grandfathered organization can therefore select and spend the platform key.
Add a test for resolveChatbotProvider('org-1', { preferId: '__platform__' }) with state.plan = 'grandfathered'. Make the resolver reject that selection before it resolves the platform provider.
🤖 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 `@tests/unit/chatbot-plan-gate.test.ts` around lines 126 - 131, Extend the
grandfathered-organization coverage around resolveChatbotProvider with preferId:
'__platform__', and update resolveChatbotProvider to reject explicit platform
selection before resolving the platform provider when canUsePlatformAi is false.
Preserve the existing “No AI config” rejection behavior and ensure the platform
key is never used for grandfathered plans.
- Add explicit type definitions for i18n link, meta, and html attributes to satisfy Unhead 3's stricter requirements. - Update `nuxt` to v4.5.2 and `typescript` to v6.0.3. - Upgrade `unhead` to v3.3.1 and `devalue` to v5.9.0. - Pin `dompurify` and `js-yaml` versions in resolutions.
- Set @drizzle-team/brocli to optional - Remove esbuild binary packages - Add oxc-parser platform-specific bindings for @dxup/nuxt
- Enforce 180-day retention for chatbot conversations. - Add database migration to index and redact entity references for automated erasure. - Implement DNS revalidation and redirect protection for custom AI endpoints to mitigate SSRF risks. - Apply 8MB body limit to chatbot uploads via custom multipart parsing. - Refactor permission checks to use granular `aiConfig` operations.
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/api/chatbot/chat.post.ts (1)
577-614: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAlways close the stream when metering fails.
If
recordAiUsagerejects, execution skips Line 614. The client can receive afinishevent but retain an unclosed stream. Catch and log metering or observability failures, then close the controller in afinallyblock.🤖 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 `@server/api/chatbot/chat.post.ts` around lines 577 - 614, Ensure the stream closes even when recordAiUsage or captureAiGeneration fails by wrapping the metering and observability calls in the existing stream-finalization flow with error logging and placing controller.close() in a finally block. Preserve the current usage payloads and event status while preventing logging or metering failures from bypassing closure.
🧹 Nitpick comments (9)
tests/unit/chatbot-attachments.test.ts (1)
41-45: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUse multibyte text for the byte-quota test.
Line 42 uses ASCII text. Its character count equals its UTF-8 byte count. A regression that uses
text.lengthinstead of byte length will pass this test.Proposed test change
- const chunk = 'x'.repeat(CHATBOT_MAX_STORED_ATTACHMENT_CHARS) + const chunk = 'é'.repeat(CHATBOT_MAX_STORED_ATTACHMENT_CHARS)🤖 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 `@tests/unit/chatbot-attachments.test.ts` around lines 41 - 45, Update the aggregate quota test around CHATBOT_MAX_STORED_ATTACHMENT_CHARS to use multibyte UTF-8 content rather than the ASCII chunk, while preserving the existing count and over-quota assertion so the test specifically detects character-counting instead of byte-length enforcement.server/utils/resume-parser.ts (3)
443-443: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCopy the buffer with
sliceinstead ofUint8Array.from.
Uint8Array.from(buffer)copies element by element. Uploads reach 8 MB (CHATBOT_MAX_UPLOAD_BYTES), so this runs on the request thread for every parse.ArrayBuffer.prototype.sliceperforms a bulk copy and produces the same detachableArrayBuffer.⚡ Proposed change
- const data = Uint8Array.from(buffer).buffer as ArrayBuffer + const data = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength, + ) as ArrayBuffer🤖 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 `@server/utils/resume-parser.ts` at line 443, Update the buffer conversion near the resume parser’s data construction to copy the existing buffer using ArrayBuffer.prototype.slice instead of Uint8Array.from. Preserve the resulting detached ArrayBuffer value and all downstream parsing behavior.
467-483: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winDistinguish limit violations from parser faults.
Lines 478 and 482 wrap worker errors and non-zero exits in
DocumentLimitError.server/api/chatbot/upload.post.tsline 62 mapsDocumentLimitErrorto HTTP 422. A worker crash, a module-resolution failure, or an out-of-memory exit is a server fault, not a client input problem, so the caller reports 422 and the operator sees no 5xx signal. Use a separate error type for infrastructure faults, and keepDocumentLimitErrorfor the page, character, timeout, and archive limits.🤖 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 `@server/utils/resume-parser.ts` around lines 467 - 483, Update the worker failure handlers in the resume parser around the message, error, and exit listeners to reject with a separate infrastructure/parser-failure error type instead of DocumentLimitError. Preserve DocumentLimitError only for explicit document limit violations such as page, character, timeout, and archive limits, and ensure the upload API can surface the new fault as a server error rather than HTTP 422.
179-185: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winRelease the PDF parser in a
finallyblock.
parser.destroy()runs after page-limit rejection, empty text, and successful parse. Ifparser.getText()rejects, that code does not run; callparser.destroy()fromfinallyand throw the page-limit error from thetryblock. Theresult.totalpage count is from the full document, so the limit check can remain based on it.🤖 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 `@server/utils/resume-parser.ts` around lines 179 - 185, Wrap the parser.getText and result-processing flow in try/finally, moving parser.destroy() into the finally block so cleanup occurs even when getText rejects. Keep the result.total page-limit check in the try block and throw DocumentLimitError there, while preserving the existing text normalization and capping behavior.server/api/chatbot/upload.post.ts (1)
90-103: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUse a distinct status for quota exhaustion.
readLimitedMultipartFilealready returns 413 for an oversized file. This handler returns 413 for an exhausted retention quota, so the client cannot separate the two causes. The quota message tells the user to wait for older uploads to expire, which matches 429. Consider 429 here, or add a machine-readable error code to the response.🤖 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 `@server/api/chatbot/upload.post.ts` around lines 90 - 103, The ChatbotAttachmentQuotaError branch in the attachment-saving handler conflates quota exhaustion with oversized-file errors by returning status 413. Change this branch to return a distinct quota-exhaustion response, preferably HTTP 429 while preserving error.message, so clients can distinguish it from readLimitedMultipartFile’s 413 response.server/utils/limitedMultipart.ts (2)
39-47: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winDestroy the request stream when the limit is exceeded.
Throwing out of the
for awaitloop leaves the socket readable. The client can continue to send the remaining body, andsetHeader(event, 'Connection', 'close')only affects the response once it is written. Destroy the request so the upload stops immediately.🛡️ Proposed change
if (received > maxRequestBytes) { setHeader(event, 'Connection', 'close') + event.node.req.destroy() throw tooLarge(maxFileBytes) }🤖 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 `@server/utils/limitedMultipart.ts` around lines 39 - 47, Update the limit-exceeded branch in the request-reading loop to destroy event.node.req before throwing tooLarge(maxFileBytes). Preserve the existing Connection header and error behavior while ensuring the request stream is terminated immediately.
90-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse
sanitizeFilenamefromserver/utils/schemas/document.
server/api/candidates/[id]/documents/index.post.tsalready imports that shared helper. The duplicated local implementation does not use the same sanitization rules, so uploads can have inconsistent stored or display filenames. ReplacelimitedMultipart.ts’s local helper with the exported one, or move this variant there if multipart uploads need different rules.🤖 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 `@server/utils/limitedMultipart.ts` around lines 90 - 92, Remove the local sanitizeFilename implementation in limitedMultipart.ts and import and reuse the exported sanitizeFilename from server/utils/schemas/document. Ensure multipart upload filename handling consistently calls this shared helper rather than maintaining separate sanitization rules.server/api/candidates/[id]/documents/index.post.ts (1)
13-13: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy liftThe parser is now isolated, but the upload reader is still unbounded.
The switch to
parseDocumentSafelyIsolatedis correct. This endpoint still reads the body withreadMultipartFormDataand checksMAX_FILE_SIZEafter the full request is buffered.server/utils/limitedMultipart.tslines 11-15 state that a post-parse size check does not protect against memory exhaustion. ApplyreadLimitedMultipartFilehere as well, or record why this endpoint keeps the buffered reader.Note that this endpoint also reads a non-file
typefield, soreadLimitedMultipartFileneeds a field accessor before it can replacereadMultipartFormDatahere.Also applies to: 142-142
🤖 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 `@server/api/candidates/`[id]/documents/index.post.ts at line 13, Replace the unbounded readMultipartFormData usage in the endpoint handler with readLimitedMultipartFile, extending that helper with a field accessor so the non-file type value remains available. Preserve the existing MAX_FILE_SIZE enforcement and downstream document parsing behavior while ensuring request data is size-limited before buffering.server/utils/ai/chatTools.ts (1)
431-434: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winCatch
DocumentLimitErrorin this tool.
parseDocumentIsolatedrejects instead of returningnullwhen a limit is hit or the worker fails. Line 434 only handles thenullcase, so the rejection escapesexecute.server/utils/resume-parser.tsline 479 builds the message asDocument parser failed: ${error.message}, which can carry worker internals into the model context. Wrap the call and return a fixed message.🛡️ Proposed change
const buf = await downloadFromS3(doc.storageKey) - const re = await parseDocumentIsolated(buf, doc.mimeType, { - maxCharacters: CHATBOT_MAX_ATTACHMENT_CHARS, - }) - if (!re?.text) throw new Error('Document could not be parsed.') + let re: Awaited<ReturnType<typeof parseDocumentIsolated>> = null + try { + re = await parseDocumentIsolated(buf, doc.mimeType, { + maxCharacters: CHATBOT_MAX_ATTACHMENT_CHARS, + }) + } + catch { + throw new Error('Document could not be parsed.') + } + if (!re?.text) throw new Error('Document could not be parsed.')🤖 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 `@server/utils/ai/chatTools.ts` around lines 431 - 434, Wrap the parseDocumentIsolated call in the tool’s execute flow with error handling that catches DocumentLimitError and worker/parser rejections, returning a fixed user-safe message instead of allowing the rejection to escape or exposing error.message. Preserve the existing null/empty-text check, and update the surrounding parsing logic near the parseDocumentIsolated call.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@app/pages/dashboard/chatbot/`[[id]].vue:
- Around line 158-161: Update the keyboard submission path in
handleSubmit/onKeyDown to reject submissions when draft.value is blank and no
attachment is present, matching the Send button’s validation condition. Perform
this check before clearing draft.value or setting shouldFollowGeneration, while
preserving submissions that include content or an attachment.
In `@server/api/candidates/`[id]/export.get.ts:
- Around line 101-120: Update the chatbot message export query around
chatbotMessageIds and chatbotRows so messages with references to other
candidates, applications, or documents are excluded or projected with unrelated
subject data redacted before export. Ensure content, reasoning, toolCalls,
sources, and conversation title do not expose mixed-subject information,
including the corresponding logic at the additional referenced location.
In `@server/api/public/jobs/`[slug]/apply.post.ts:
- Line 576: Bound calls to parseDocumentSafelyIsolated in the public application
route with a process-wide concurrency limiter or worker pool, covering both
parsing call sites. Queue or reject work once the configured limit is reached,
and ensure permits are released when parsing completes or fails.
In `@server/database/migrations/0061_shallow_marrow.sql`:
- Around line 66-72: Update the migration after the chatbot_message redaction
identified by stale_messages to delete all corresponding rows from
chatbot_message_entity_reference, using each row’s message_id to target the
redacted messages. Preserve the existing payload redaction and ensure references
are removed within the same migration.
In `@server/tasks/retention-cleanup.ts`:
- Around line 10-13: Update run() so pruneExpiredChatbotConversations() executes
only when GDPR_CLEANUP_ENABLED is explicitly enabled, matching the kill-switch
behavior enforced by runRetentionCleanup(). Preserve the returned
chatbotConversationsDeleted field with an appropriate zero/empty value when
cleanup is disabled.
In `@server/utils/ai/safeEndpoint.ts`:
- Around line 127-130: Update safeAiEndpointFetch and its underlying request
path so the validated DNS address is reused for the connection instead of
resolving the hostname again; preserve the original hostname for TLS/SNI/Host
handling and retain the existing network egress deny rules. Ensure
assertSafeAiEndpoint and the subsequent fetch use the same validated resolution
result.
In `@server/utils/chatbotAttachments.ts`:
- Around line 70-82: The attachment retention flow must not delete existing
entries before byte-quota validation. In the per-user count handling, calculate
projected user and organization usage and run both quota checks before evicting
any oldest count-limit entries; only perform deletions after all validations
pass, preserving the existing quota errors and retention behavior.
In `@server/utils/chatbotRetention.ts`:
- Around line 18-20: Add the retention cutoff predicate to the delete query in
chatbot retention, combining the existing ID filter with updatedAt <= cutoff
before returning deleted IDs. Ensure rows refreshed after the candidate query
are not deleted, while preserving the existing expired-row deletion flow.
In `@server/utils/erasure.ts`:
- Around line 365-395: Update the requirePurgeEligible flow around the
advisory-lock and S3 cleanup sequence to atomically create a durable purge claim
before deleting any S3 objects. Make reapplication detect and honor that claim,
then move candidate deletion finalization into a retryable workflow so a failed
or rolled-back guarded delete cannot leave a restored candidate without its
files.
In `@server/utils/limitedMultipart.ts`:
- Line 65: Update the header decoding in the multipart parsing logic around the
body subarray and LimitedMultipartFile.filename assignment to use UTF-8 instead
of latin1. Preserve the existing header-name parsing, boundary handling, and
filename propagation behavior.
In `@server/utils/resume-parser.ts`:
- Around line 403-416: Update parseDocumentIsolated so the returned sections
field is populated by applying extractSections to the normalized text, matching
the main-thread parser behavior. Preserve the existing null handling and
metadata construction while replacing the unconditional empty sections array.
- Around line 499-501: Update the isolated parsing path around
parseDocumentIsolated in parseDocumentDetailed so that a successful parse with
no extracted text emits the existing resume_parser.no_text_extracted log before
returning { ok: false, reason: 'empty' }. Preserve the current parse_failed
handling and ensure the event remains distinct from failures.
- Around line 352-353: Update the isolated worker setup around
ISOLATED_PARSER_SOURCE so its runtime mammoth and word-extractor dependencies
are available in production: either configure Nitro to bundle/copy both packages
into .output/server/node_modules, or pass their resolved absolute import
specifiers through workerData and load them with await import(...). Ensure the
worker no longer relies on unresolved bare require calls.
In `@shared/chatbot.ts`:
- Around line 18-32: Update CHATBOT_MAX_STORED_BYTES_PER_USER so it is at least
CHATBOT_MAX_ATTACHMENTS_PER_MESSAGE * CHATBOT_MAX_STORED_ATTACHMENT_CHARS * 4,
ensuring a message at the documented attachment and character limits fits within
the per-user UTF-8 byte quota. Keep the existing per-organization cap and
character-based storage limits unchanged.
---
Outside diff comments:
In `@server/api/chatbot/chat.post.ts`:
- Around line 577-614: Ensure the stream closes even when recordAiUsage or
captureAiGeneration fails by wrapping the metering and observability calls in
the existing stream-finalization flow with error logging and placing
controller.close() in a finally block. Preserve the current usage payloads and
event status while preventing logging or metering failures from bypassing
closure.
---
Nitpick comments:
In `@server/api/candidates/`[id]/documents/index.post.ts:
- Line 13: Replace the unbounded readMultipartFormData usage in the endpoint
handler with readLimitedMultipartFile, extending that helper with a field
accessor so the non-file type value remains available. Preserve the existing
MAX_FILE_SIZE enforcement and downstream document parsing behavior while
ensuring request data is size-limited before buffering.
In `@server/api/chatbot/upload.post.ts`:
- Around line 90-103: The ChatbotAttachmentQuotaError branch in the
attachment-saving handler conflates quota exhaustion with oversized-file errors
by returning status 413. Change this branch to return a distinct
quota-exhaustion response, preferably HTTP 429 while preserving error.message,
so clients can distinguish it from readLimitedMultipartFile’s 413 response.
In `@server/utils/ai/chatTools.ts`:
- Around line 431-434: Wrap the parseDocumentIsolated call in the tool’s execute
flow with error handling that catches DocumentLimitError and worker/parser
rejections, returning a fixed user-safe message instead of allowing the
rejection to escape or exposing error.message. Preserve the existing
null/empty-text check, and update the surrounding parsing logic near the
parseDocumentIsolated call.
In `@server/utils/limitedMultipart.ts`:
- Around line 39-47: Update the limit-exceeded branch in the request-reading
loop to destroy event.node.req before throwing tooLarge(maxFileBytes). Preserve
the existing Connection header and error behavior while ensuring the request
stream is terminated immediately.
- Around line 90-92: Remove the local sanitizeFilename implementation in
limitedMultipart.ts and import and reuse the exported sanitizeFilename from
server/utils/schemas/document. Ensure multipart upload filename handling
consistently calls this shared helper rather than maintaining separate
sanitization rules.
In `@server/utils/resume-parser.ts`:
- Line 443: Update the buffer conversion near the resume parser’s data
construction to copy the existing buffer using ArrayBuffer.prototype.slice
instead of Uint8Array.from. Preserve the resulting detached ArrayBuffer value
and all downstream parsing behavior.
- Around line 467-483: Update the worker failure handlers in the resume parser
around the message, error, and exit listeners to reject with a separate
infrastructure/parser-failure error type instead of DocumentLimitError. Preserve
DocumentLimitError only for explicit document limit violations such as page,
character, timeout, and archive limits, and ensure the upload API can surface
the new fault as a server error rather than HTTP 422.
- Around line 179-185: Wrap the parser.getText and result-processing flow in
try/finally, moving parser.destroy() into the finally block so cleanup occurs
even when getText rejects. Keep the result.total page-limit check in the try
block and throw DocumentLimitError there, while preserving the existing text
normalization and capping behavior.
In `@tests/unit/chatbot-attachments.test.ts`:
- Around line 41-45: Update the aggregate quota test around
CHATBOT_MAX_STORED_ATTACHMENT_CHARS to use multibyte UTF-8 content rather than
the ASCII chunk, while preserving the existing count and over-quota assertion so
the test specifically detects character-counting instead of byte-length
enforcement.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e6077caf-9645-4409-a27e-53bf096d4fee
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (45)
DATA-RETENTION.mdSECURITY.mdapp/app.vueapp/components/AppTopBar.vueapp/pages/dashboard/chatbot/[[id]].vueapp/pages/dashboard/settings/ai/[id].vueapp/pages/dashboard/settings/ai/index.vueapp/pages/dashboard/settings/ai/new.vuepackage.jsonserver/api/ai-config/[id].delete.tsserver/api/ai-config/[id].patch.tsserver/api/ai-config/[id]/set-default.post.tsserver/api/ai-config/[id]/test-connection.post.tsserver/api/ai-config/index.post.tsserver/api/candidates/[id]/documents/index.post.tsserver/api/candidates/[id]/export.get.tsserver/api/candidates/extract-cv.post.tsserver/api/chatbot/chat.post.tsserver/api/chatbot/upload.post.tsserver/api/public/jobs/[slug]/apply.post.tsserver/database/migrations/0061_shallow_marrow.sqlserver/database/migrations/meta/0061_snapshot.jsonserver/database/migrations/meta/_journal.jsonserver/database/schema/app.tsserver/tasks/retention-cleanup.tsserver/utils/ai/chatTools.tsserver/utils/ai/provider.tsserver/utils/ai/safeEndpoint.tsserver/utils/chatbotAttachments.tsserver/utils/chatbotRetention.tsserver/utils/chatbotSources.tsserver/utils/document-parser.tsserver/utils/erasure.tsserver/utils/limitedMultipart.tsserver/utils/resume-parser.tsserver/utils/schemas/scoring.tsshared/chatbot.tsshared/permissions.tstests/unit/ai-config-schema.test.tstests/unit/ai-provider.test.tstests/unit/ai-safe-endpoint.test.tstests/unit/chatbot-attachments.test.tstests/unit/chatbot-job-scope.test.tstests/unit/erasure.test.tstests/unit/resume-parser.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- server/database/migrations/meta/_journal.json
| if (isStreaming.value || chatbotQuotaExhausted.value) return | ||
| const content = draft.value | ||
| draft.value = '' | ||
| shouldFollowGeneration.value = true |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Prevent blank keyboard submissions.
Line 158 does not reject a blank draft with no attachment. onKeyDown can call handleSubmit, which then calls send(''). Match the Send button condition before clearing the draft.
Proposed fix
async function handleSubmit() {
- if (isStreaming.value || chatbotQuotaExhausted.value) return
+ if (
+ isStreaming.value
+ || chatbotQuotaExhausted.value
+ || (!draft.value.trim() && pendingAttachments.value.length === 0)
+ ) return
const content = draft.value📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (isStreaming.value || chatbotQuotaExhausted.value) return | |
| const content = draft.value | |
| draft.value = '' | |
| shouldFollowGeneration.value = true | |
| if ( | |
| isStreaming.value | |
| || chatbotQuotaExhausted.value | |
| || (!draft.value.trim() && pendingAttachments.value.length === 0) | |
| ) return | |
| const content = draft.value | |
| draft.value = '' | |
| shouldFollowGeneration.value = true |
🤖 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/pages/dashboard/chatbot/`[[id]].vue around lines 158 - 161, Update the
keyboard submission path in handleSubmit/onKeyDown to reject submissions when
draft.value is blank and no attachment is present, matching the Send button’s
validation condition. Perform this check before clearing draft.value or setting
shouldFollowGeneration, while preserving submissions that include content or an
attachment.
| const chatbotMessageIds = [...new Set(chatbotReferences.map(reference => reference.messageId))] | ||
| const chatbotRows = chatbotMessageIds.length > 0 | ||
| ? await db.select({ | ||
| conversationId: chatbotConversation.id, | ||
| conversationTitle: chatbotConversation.title, | ||
| messageId: chatbotMessage.id, | ||
| role: chatbotMessage.role, | ||
| content: chatbotMessage.content, | ||
| reasoning: chatbotMessage.reasoning, | ||
| toolCalls: chatbotMessage.toolCalls, | ||
| sources: chatbotMessage.sources, | ||
| createdAt: chatbotMessage.createdAt, | ||
| }) | ||
| .from(chatbotMessage) | ||
| .innerJoin(chatbotConversation, eq(chatbotMessage.conversationId, chatbotConversation.id)) | ||
| .where(and( | ||
| eq(chatbotMessage.organizationId, orgId), | ||
| inArray(chatbotMessage.id, chatbotMessageIds), | ||
| )) | ||
| : [] |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Do not export mixed-subject chatbot messages unchanged.
These lines select the complete message when it has one reference in the candidate graph. A message can also reference another candidate, application, or document. The export then returns its full content, reasoning, toolCalls, sources, and conversation title.
Exclude mixed-subject messages, or create a redacted projection that removes unrelated subject data before export.
Also applies to: 133-138
🤖 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 `@server/api/candidates/`[id]/export.get.ts around lines 101 - 120, Update the
chatbot message export query around chatbotMessageIds and chatbotRows so
messages with references to other candidates, applications, or documents are
excluded or projected with unrelated subject data redacted before export. Ensure
content, reasoning, toolCalls, sources, and conversation title do not expose
mixed-subject information, including the corresponding logic at the additional
referenced location.
|
|
||
| // Parse document content (best-effort — does not block upload) | ||
| const parsedContent = await parseDocument(file.data, mimeType) | ||
| const parsedContent = await parseDocumentSafelyIsolated(file.data, mimeType) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Bound worker concurrency on this public endpoint.
Each parseDocumentSafelyIsolated call starts a new worker thread. server/utils/resume-parser.ts line 451 allows each worker up to 128 MB of old-space, and line 443 copies the file bytes before transfer. This route is public and accepts multiple files per application, so concurrent submissions can start many workers at once and exhaust host memory or saturate the thread pool.
Add a global concurrency limit or a worker pool in front of parseDocumentIsolated, and reject or queue requests beyond it.
Run the following script to check for an existing limiter on this route:
#!/bin/bash
# Look for rate limiting or concurrency control on the public apply route.
rg -nP --type=ts -C4 'createRateLimiter|rateLimit|semaphore|p-limit|concurrenc' server/api/public server/utilsAlso applies to: 635-635
🤖 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 `@server/api/public/jobs/`[slug]/apply.post.ts at line 576, Bound calls to
parseDocumentSafelyIsolated in the public application route with a process-wide
concurrency limiter or worker pool, covering both parsing call sites. Queue or
reject work once the configured limit is reached, and ensure permits are
released when parsing completes or fails.
| UPDATE "chatbot_message" | ||
| SET "content" = '[Redacted because this message referenced erased candidate data.]', | ||
| "reasoning" = NULL, | ||
| "tool_calls" = NULL, | ||
| "sources" = NULL, | ||
| "attachments" = NULL | ||
| WHERE "id" IN (SELECT "message_id" FROM stale_messages);--> statement-breakpoint |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Delete entity references for redacted messages.
Lines 66-72 redact the message payload but retain its chatbot_message_entity_reference rows. A redacted message with another valid reference can still expose an erased entity ID through server/api/candidates/[id]/export.get.ts.
Delete all entity-reference rows for each redacted message in the same migration.
🤖 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 `@server/database/migrations/0061_shallow_marrow.sql` around lines 66 - 72,
Update the migration after the chatbot_message redaction identified by
stale_messages to delete all corresponding rows from
chatbot_message_entity_reference, using each row’s message_id to target the
redacted messages. Preserve the existing payload redaction and ensure references
are removed within the same migration.
| async run() { | ||
| const chatbotConversationsDeleted = await pruneExpiredChatbotConversations() | ||
| const result = await runRetentionCleanup({ source: 'scheduled_task' }) | ||
| return { result } | ||
| return { result: { ...result, chatbotConversationsDeleted } } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Honor the cleanup kill switch for chatbot conversations.
This task deletes chatbot conversations before runRetentionCleanup() checks GDPR_CLEANUP_ENABLED. Therefore, an unset or false value still deletes chatbot data. This conflicts with the documented instance-level pause for automatic cleanup.
Gate chatbot pruning with the same explicit setting, or document and implement a separate chatbot-retention control.
🤖 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 `@server/tasks/retention-cleanup.ts` around lines 10 - 13, Update run() so
pruneExpiredChatbotConversations() executes only when GDPR_CLEANUP_ENABLED is
explicitly enabled, matching the kill-switch behavior enforced by
runRetentionCleanup(). Preserve the returned chatbotConversationsDeleted field
with an appropriate zero/empty value when cleanup is disabled.
| if (headersEnd < 0 || headersEnd - partStart > 16 * 1024) { | ||
| throw createError({ statusCode: 400, statusMessage: 'Malformed multipart data.' }) | ||
| } | ||
| const headers = body.subarray(partStart, headersEnd).toString('latin1') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Decode the part headers as UTF-8, not latin1.
Browsers send filename in Content-Disposition as raw UTF-8 bytes. Line 65 decodes the header block with latin1, so a filename such as Résumé.pdf becomes Résumé.pdf. That value is stored and shown to users through LimitedMultipartFile.filename.
The ASCII-only header names and the boundary parsing are unaffected by a UTF-8 decode, because UTF-8 is ASCII-compatible.
🌍 Proposed change
- const headers = body.subarray(partStart, headersEnd).toString('latin1')
+ const headers = body.subarray(partStart, headersEnd).toString('utf8')Also applies to: 71-73
🤖 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 `@server/utils/limitedMultipart.ts` at line 65, Update the header decoding in
the multipart parsing logic around the body subarray and
LimitedMultipartFile.filename assignment to use UTF-8 instead of latin1.
Preserve the existing header-name parsing, boundary handling, and filename
propagation behavior.
| const ISOLATED_PARSER_SOURCE = String.raw` | ||
| const { parentPort, workerData } = require('node:worker_threads') |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check Nitro/Nuxt config for externals and how server deps are bundled.
fd -H -t f 'nuxt.config.*' -x cat -n {}
fd -H -t f 'nitro.config.*' -x cat -n {}
# Check whether mammoth / word-extractor / pdf-parse are runtime deps.
rg -n '"(mammoth|word-extractor|pdf-parse)"' package.jsonRepository: reqcore-inc/reqcore
Length of output: 18796
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package manager files =="
git ls-files 'package.json' 'pnpm-lock.yaml' 'yarn.lock' 'package-lock.json' 'bun.lockb' | sed -n '1,120p'
echo
echo "== resume-parser outline =="
ast-grep outline server/utils/resume-parser.ts --view expanded || true
echo
echo "== relevant resume-parser sections =="
sed -n '300,420p' server/utils/resume-parser.ts
echo
echo "== workerThread/eval usage in resume-parser =="
rg -n "new Worker|createWorker|parentPort|mammoth|word-extractor|require\\(|parseDocumentDetailedIsolated|parseDocumentIsolated" server/utils/resume-parser.tsRepository: reqcore-inc/reqcore
Length of output: 7880
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read package metadata and lockfile entries without installing/parsing lockfile.
python3 - <<'PY'
import json, pathlib, re
pkg_path = pathlib.Path("package.json")
pkg = json.loads(pkg_path.read_text())
for key in ["dependencies","devDependencies","peerDependencies","optionalDependencies"]:
values = pkg.get(key, {})
hits = {k: values[k] for k in ["mammoth","word-extractor","pdf-parse"] if k in values}
if hits:
print(f"[{key}]")
for k,v in hits.items():
print(f"{k}={v}")
lock = pathlib.Path("pnpm-lock.yaml")
if lock.exists():
data = lock.read_text(errors="ignore")
for name in ["mammoth","word-extractor","pdf-parse"]:
print(f"\n= {name} in pnpm-lock.yaml =")
pattern = re.compile(rf"^\s+(?:/{name}\s+|/{name}@[^/]+/[^:\n]+:\s+$)", re.M)
# Print nearby lines around each matching specifier
lines = data.splitlines()
for i,l in enumerate(lines):
if f"/{name}" in l and ": " in l:
print("line", i+1, ":", l)
for j in range(max(0, i-1), min(len(lines), i+20)):
print(f"{j+1}: {lines[j]}")
PYRepository: reqcore-inc/reqcore
Length of output: 227
Keep worker deps in the Nitro server bundle.
The isolated worker evaluates require('mammoth') and require('word-extractor') without a source file path. Ensure these runtime deps are copied into .output/server/node_modules for production, or pass resolved absolute import specifiers through workerData and load them with await import(...).
🤖 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 `@server/utils/resume-parser.ts` around lines 352 - 353, Update the isolated
worker setup around ISOLATED_PARSER_SOURCE so its runtime mammoth and
word-extractor dependencies are available in production: either configure Nitro
to bundle/copy both packages into .output/server/node_modules, or pass their
resolved absolute import specifiers through workerData and load them with await
import(...). Ensure the worker no longer relies on unresolved bare require
calls.
| const text = normalize(raw, maxCharacters) | ||
| if (!text) return null | ||
| return { | ||
| text, | ||
| sections: [], | ||
| metadata: { | ||
| pageCount, | ||
| wordCount: words(text), | ||
| characterCount: text.length, | ||
| extractedAt: new Date().toISOString(), | ||
| parserVersion: '1.1', | ||
| sourceFormat, | ||
| }, | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
The isolated parser always returns empty sections, which changes persisted document data.
The main-thread parsers populate sections with extractSections(text) (lines 193, 219, 250). The worker returns sections: []. All production callers now use the isolated path:
server/utils/document-parser.tsline 19 persists the result intodocument.parsedContent.server/api/candidates/[id]/documents/index.post.tsline 142 persists it at upload time.server/api/public/jobs/[slug]/apply.post.tslines 576 and 635 persist it.server/api/candidates/extract-cv.post.tsline 81 returns it to the client.
Newly parsed documents therefore lose section detection, while older rows keep it. Any consumer that reads parsedContent.sections will see an empty array. Either run extractSections on the returned text in parseDocumentIsolated, or confirm that no consumer depends on sections.
🐛 Proposed fix: restore sections on the host side
finish(() => {
- if (message.ok) resolve(message.parsed ?? null)
+ if (message.ok) {
+ const parsed = message.parsed ?? null
+ if (parsed) parsed.sections = extractSections(parsed.text)
+ resolve(parsed)
+ }
else reject(new DocumentLimitError(Run the following script to find consumers of sections:
#!/bin/bash
# Find readers of parsedContent.sections and ParsedResume.sections
rg -nP --type=ts -C3 '\bsections\b' -g '!**/node_modules/**'
rg -nP --type=vue -C3 '\bsections\b' 2>/dev/null || rg -nP -g '*.vue' -C3 '\bsections\b'🤖 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 `@server/utils/resume-parser.ts` around lines 403 - 416, Update
parseDocumentIsolated so the returned sections field is populated by applying
extractSections to the normalized text, matching the main-thread parser
behavior. Preserve the existing null handling and metadata construction while
replacing the unconditional empty sections array.
| try { | ||
| const parsed = await parseDocumentIsolated(buffer, mimeType, limits) | ||
| return parsed ? { ok: true, parsed } : { ok: false, reason: 'empty' } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Keep the no_text_extracted log on the isolated path.
parseDocumentDetailed logs resume_parser.no_text_extracted when a readable file yields no text (lines 135-143). The comment there states that the event must stay separable from resume_parser.parse_failed in PostHog. The isolated path returns { ok: false, reason: 'empty' } without that log, so scanned or image-only CVs become invisible in analytics once callers move to this function.
🩹 Proposed fix
const parsed = await parseDocumentIsolated(buffer, mimeType, limits)
- return parsed ? { ok: true, parsed } : { ok: false, reason: 'empty' }
+ if (parsed) return { ok: true, parsed }
+ logWarn('resume_parser.no_text_extracted', {
+ mime_type: mimeType,
+ byte_size: buffer.length,
+ })
+ return { ok: false, reason: 'empty' }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const parsed = await parseDocumentIsolated(buffer, mimeType, limits) | |
| return parsed ? { ok: true, parsed } : { ok: false, reason: 'empty' } | |
| try { | |
| const parsed = await parseDocumentIsolated(buffer, mimeType, limits) | |
| if (parsed) return { ok: true, parsed } | |
| logWarn('resume_parser.no_text_extracted', { | |
| mime_type: mimeType, | |
| byte_size: buffer.length, | |
| }) | |
| return { ok: false, reason: 'empty' } |
🤖 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 `@server/utils/resume-parser.ts` around lines 499 - 501, Update the isolated
parsing path around parseDocumentIsolated in parseDocumentDetailed so that a
successful parse with no extracted text emits the existing
resume_parser.no_text_extracted log before returning { ok: false, reason:
'empty' }. Preserve the current parse_failed handling and ensure the event
remains distinct from failures.
| /** Extracted text is capped before it enters the in-memory attachment store. */ | ||
| export const CHATBOT_MAX_STORED_ATTACHMENT_CHARS = 40_000 | ||
|
|
||
| /** Aggregate UTF-8 text retained per user and per organization. */ | ||
| export const CHATBOT_MAX_STORED_BYTES_PER_USER = 512 * 1024 | ||
| export const CHATBOT_MAX_STORED_BYTES_PER_ORG = 5 * 1024 * 1024 | ||
|
|
||
| /** Maximum number of attachments a user can include in a single message. */ | ||
| export const CHATBOT_MAX_ATTACHMENTS_PER_MESSAGE = 5 | ||
|
|
||
| /** Maximum number of messages kept in a single conversation. */ | ||
| export const CHATBOT_MAX_MESSAGES = 50 | ||
|
|
||
| /** Maximum characters of attachment text injected into the model prompt. */ | ||
| export const CHATBOT_MAX_ATTACHMENT_CHARS = 40_000 | ||
| export const CHATBOT_MAX_ATTACHMENT_CHARS = CHATBOT_MAX_STORED_ATTACHMENT_CHARS |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
The per-message limit can exceed the per-user byte cap.
CHATBOT_MAX_ATTACHMENTS_PER_MESSAGE is 5 and CHATBOT_MAX_STORED_ATTACHMENT_CHARS is 40 000. server/utils/chatbotAttachments.ts measures the quota in UTF-8 bytes. For non-Latin text at 3 bytes per character, one message can require up to 600 KB, which is above CHATBOT_MAX_STORED_BYTES_PER_USER (512 KB). The user then receives a quota error for a message that respects every documented per-file limit.
Raise the per-user cap to at least CHATBOT_MAX_ATTACHMENTS_PER_MESSAGE * CHATBOT_MAX_STORED_ATTACHMENT_CHARS * 4, or cap the stored text in bytes rather than characters.
🤖 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 `@shared/chatbot.ts` around lines 18 - 32, Update
CHATBOT_MAX_STORED_BYTES_PER_USER so it is at least
CHATBOT_MAX_ATTACHMENTS_PER_MESSAGE * CHATBOT_MAX_STORED_ATTACHMENT_CHARS * 4,
ensuring a message at the documented attachment and character limits fits within
the per-user UTF-8 byte quota. Keep the existing per-organization cap and
character-based storage limits unchanged.
- Separate assistant spend from analysis spend: assistant usage is now metered in credits (see credits.ts) and is no longer part of the analysis dollar budget. - Introduce an up-front credit reservation system (`reserveChatbotUsage` / `settleChatbotUsage`) to prevent concurrent turns from overdrawing the org's allowance. - Replace the turn-count quota with a percentage-based credit meter to keep the true cost per turn private. - Add a vetted model catalogue (`shared/chatbot-models.ts`) with price data to inform the credit multiplier shown in the picker. - Backfill historical assistant turns with an estimated credit charge to ensure no org starts with a "fresh" allowance in the new system. - Add `chatbot_preference` for users to star their default model.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
app/composables/useChatbot.ts (2)
151-176: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueConfirm that a failed preferences fetch is acceptable as "no default".
Line 158 maps any failure of
/api/chatbot/preferencesto{ defaultModel: null }.resetModelSelectionToDefault()then clears the composer selection at line 171. The user sees the star disappear and no error. The write path is unaffected, so no wrong value is persisted. If a silent reset is not the intent, surface the failure throughreportErrorwhile keeping the null fallback.🤖 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/composables/useChatbot.ts` around lines 151 - 176, Update loadAll and the /api/chatbot/preferences fallback so a failed preferences request is reported through reportError while retaining the { defaultModel: null } fallback and existing resetModelSelectionToDefault behavior.
183-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDefine platform-engine availability once. Three sites repeat the same predicate over
platformAiConfig: it must be non-null and not explicitly disabled. The composable ownsplatformAiConfig, so a single exported computed there removes the drift risk.
app/composables/useChatbot.ts#L183-L190: replace the inlineplatformAvailablelocal inresetModelSelectionToDefaultwith a sharedplatformAvailablecomputed and export it fromuseChatbot().app/composables/useChatbot.ts#L305-L310: use the same shared computed innewConversationinstead of recomputing the predicate.app/components/ChatbotModelPicker.vue#L56-L59: destructureplatformAvailablefromuseChatbot()instead of defining a local computed.🤖 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/composables/useChatbot.ts` around lines 183 - 190, Define and export one shared platformAvailable computed in useChatbot() based on platformAiConfig being non-null and not explicitly disabled. In app/composables/useChatbot.ts lines 183-190, replace the local predicate in resetModelSelectionToDefault; in lines 305-310, reuse the shared computed in newConversation; and in app/components/ChatbotModelPicker.vue lines 56-59, destructure platformAvailable from useChatbot() instead of defining a local computed.app/components/ChatbotModelPicker.vue (1)
143-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd disclosure semantics to the model menu.
The trigger button does not expose
aria-expanded, and the menu closes only on an outside click. Screen reader users get no open/closed state, and keyboard users cannot dismiss the menu with Escape.♿ Proposed accessibility improvements
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-900 px-2.5 py-1.5 text-xs font-medium text-surface-700 dark:text-surface-200 hover:border-brand-300 dark:hover:border-brand-700 cursor-pointer transition-colors" :title="selectedModelChoice?.description ?? selectedConfig?.model ?? defaultChatbotConfig?.model ?? 'No model configured'" + :aria-expanded="open" + aria-haspopup="menu" `@click`="open = !open" + `@keydown.esc`="open = false" >Add the same
@keydown.esc="open = false"handler on the menu container, or register a document-levelkeydownlistener next toonWindowClick.🤖 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/components/ChatbotModelPicker.vue` around lines 143 - 166, Update the model picker trigger button to expose its state with aria-expanded bound to open, and add Escape-key handling to the open menu container so it sets open to false. Use the existing open state and menu markup around ChatbotVendorIcon without changing outside-click behavior.server/database/migrations/0062_confused_the_liberteens.sql (2)
2-2: 🩺 Stability & Availability | 🔵 TrivialPlan the index build for a large
ai_usage_eventtable.
CREATE INDEXwithoutCONCURRENTLYholds aSHARElock and blocks writes toai_usage_eventfor the whole build.ai_usage_eventis an append-heavy ledger, so every assistant turn blocks during deployment.CONCURRENTLYcannot run inside the migration transaction, so either accept the write pause during a maintenance window or move this index to an out-of-band step.🤖 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 `@server/database/migrations/0062_confused_the_liberteens.sql` at line 2, Update the migration containing the ai_usage_event_org_feature_created_idx creation to use an operationally safe large-table rollout: either schedule the blocking CREATE INDEX for a documented maintenance window, or remove it from the transactional migration and provide it as an out-of-band CREATE INDEX CONCURRENTLY step, ensuring the concurrent build runs outside a migration transaction.Source: Linters/SAST tools
11-21: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoffConsider batching the backfill for large ledgers.
Both
UPDATEstatements rewrite every historicalchatbot_messagerow in one transaction. On a largeai_usage_eventtable this holds row locks and grows WAL for the whole migration. If the table is already large in production, run the backfill in bounded batches outside the schema migration.🤖 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 `@server/database/migrations/0062_confused_the_liberteens.sql` around lines 11 - 21, Move the two chatbot_message credits backfills out of the single transactional schema migration and implement them as bounded batches, preserving the existing cost-based calculation and 30-credit fallback for still-null rows. Ensure each batch commits independently and continues until no matching rows remain, using a stable unique-row ordering or equivalent mechanism to avoid repeatedly scanning or updating the same records.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@server/utils/ai/credits.ts`:
- Around line 114-117: Update creditPercentUsed so values below the fully spent
allowance cannot round to 100; reserve 100 exclusively for used values at or
above allowance while preserving clamping to 0–100 and the invalid/non-positive
allowance behavior.
In `@server/utils/ai/platformConfig.ts`:
- Around line 126-128: Update MODEL_PRICING to include entries matching every
assistant catalogue model’s normalized bare id, including claude-haiku-4.5 and
the x-ai/grok-* models, so the model selected by platformModel or
opts.modelOverride is priced before platform-paid billing; alternatively remove
any catalogue models that cannot be priced.
In `@tests/unit/ai-budget-ledgers.test.ts`:
- Around line 76-86: Update the db stub used by the test double around queryKind
so credit queries apply the supplied createdAt and billingMode predicates
instead of always returning state.credits. Add representative historical,
platform, and BYOK credit rows, then assert getChatbotCreditUsage counts only
the rows required by each paid usage plan.
---
Nitpick comments:
In `@app/components/ChatbotModelPicker.vue`:
- Around line 143-166: Update the model picker trigger button to expose its
state with aria-expanded bound to open, and add Escape-key handling to the open
menu container so it sets open to false. Use the existing open state and menu
markup around ChatbotVendorIcon without changing outside-click behavior.
In `@app/composables/useChatbot.ts`:
- Around line 151-176: Update loadAll and the /api/chatbot/preferences fallback
so a failed preferences request is reported through reportError while retaining
the { defaultModel: null } fallback and existing resetModelSelectionToDefault
behavior.
- Around line 183-190: Define and export one shared platformAvailable computed
in useChatbot() based on platformAiConfig being non-null and not explicitly
disabled. In app/composables/useChatbot.ts lines 183-190, replace the local
predicate in resetModelSelectionToDefault; in lines 305-310, reuse the shared
computed in newConversation; and in app/components/ChatbotModelPicker.vue lines
56-59, destructure platformAvailable from useChatbot() instead of defining a
local computed.
In `@server/database/migrations/0062_confused_the_liberteens.sql`:
- Line 2: Update the migration containing the
ai_usage_event_org_feature_created_idx creation to use an operationally safe
large-table rollout: either schedule the blocking CREATE INDEX for a documented
maintenance window, or remove it from the transactional migration and provide it
as an out-of-band CREATE INDEX CONCURRENTLY step, ensuring the concurrent build
runs outside a migration transaction.
- Around line 11-21: Move the two chatbot_message credits backfills out of the
single transactional schema migration and implement them as bounded batches,
preserving the existing cost-based calculation and 30-credit fallback for
still-null rows. Ensure each batch commits independently and continues until no
matching rows remain, using a stable unique-row ordering or equivalent mechanism
to avoid repeatedly scanning or updating the same records.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9bccd4fc-7bed-4d1c-ab29-5d868c07ef1a
📒 Files selected for processing (40)
app/components/ChatbotModelPicker.vueapp/components/ChatbotQuotaUpsellCard.vueapp/components/ChatbotVendorIcon.vueapp/components/FreePlanUpgradeMenu.vueapp/components/FreePlanUpsellCard.vueapp/components/PublicPricingSection.vueapp/components/UsageMeterBar.vueapp/composables/useChatbot.tsapp/composables/useChatbotQuota.tsapp/pages/dashboard/chatbot/[[id]].vueserver/api/chatbot/chat.post.tsserver/api/chatbot/conversations/[id].patch.tsserver/api/chatbot/conversations/index.post.tsserver/api/chatbot/preferences.get.tsserver/api/chatbot/preferences.patch.tsserver/database/migrations/0061_shallow_marrow.sqlserver/database/migrations/0062_confused_the_liberteens.sqlserver/database/migrations/0063_sleepy_king_bedlam.sqlserver/database/migrations/0064_bumpy_the_order.sqlserver/database/migrations/meta/0062_snapshot.jsonserver/database/migrations/meta/0063_snapshot.jsonserver/database/migrations/meta/0064_snapshot.jsonserver/database/migrations/meta/_journal.jsonserver/database/schema/app.tsserver/utils/ai/budget.tsserver/utils/ai/credits.tsserver/utils/ai/platformConfig.tsserver/utils/ai/pricing.tsserver/utils/ai/resolveProvider.tsserver/utils/ai/usage.tsserver/utils/billing/usage.tsserver/utils/chatbotConversation.tsshared/billing.tsshared/chatbot-models.tsshared/chatbot.tsshared/feature-flags.tstests/unit/ai-budget-ledgers.test.tstests/unit/ai-chatbot-credits.test.tstests/unit/chatbot-conversation-model.test.tstests/unit/chatbot-model-catalogue.test.ts
🚧 Files skipped from review as they are similar to previous changes (11)
- app/components/PublicPricingSection.vue
- app/components/FreePlanUpsellCard.vue
- server/api/chatbot/conversations/index.post.ts
- app/components/FreePlanUpgradeMenu.vue
- server/utils/chatbotConversation.ts
- server/utils/ai/resolveProvider.ts
- shared/feature-flags.ts
- app/pages/dashboard/chatbot/[[id]].vue
- server/database/migrations/0061_shallow_marrow.sql
- server/api/chatbot/chat.post.ts
- server/database/schema/app.ts
| export function creditPercentUsed(used: number, allowance: number): number { | ||
| if (!Number.isFinite(allowance) || allowance <= 0) return 0 | ||
| return Math.min(100, Math.max(0, Math.round((used / allowance) * 100))) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not round up to 100 before the allowance is spent.
Math.round returns 100 when used/allowance is 0.995 or higher. useChatbotQuota.exhausted treats 100 as exhausted and replaces the composer, but assertChatbotCredits still allows turns because used < allowance. The customer loses the last part of the allowance. Reserve 100 for real exhaustion.
🐛 Proposed fix
export function creditPercentUsed(used: number, allowance: number): number {
if (!Number.isFinite(allowance) || allowance <= 0) return 0
- return Math.min(100, Math.max(0, Math.round((used / allowance) * 100)))
+ if (used >= allowance) return 100
+ // Floor below the cap: 100 must mean "spent", because the client gates on it.
+ return Math.max(0, Math.floor((used / allowance) * 100))
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function creditPercentUsed(used: number, allowance: number): number { | |
| if (!Number.isFinite(allowance) || allowance <= 0) return 0 | |
| return Math.min(100, Math.max(0, Math.round((used / allowance) * 100))) | |
| } | |
| export function creditPercentUsed(used: number, allowance: number): number { | |
| if (!Number.isFinite(allowance) || allowance <= 0) return 0 | |
| if (used >= allowance) return 100 | |
| // Floor below the cap: 100 must mean "spent", because the client gates on it. | |
| return Math.max(0, Math.floor((used / allowance) * 100)) | |
| } |
🤖 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 `@server/utils/ai/credits.ts` around lines 114 - 117, Update creditPercentUsed
so values below the fully spent allowance cannot round to 100; reserve 100
exclusively for used values at or above allowance while preserving clamping to
0–100 and the invalid/non-positive allowance behavior.
| vi.stubGlobal('db', { | ||
| select: (projection: unknown) => ({ | ||
| from: (table: unknown) => { | ||
| state.queriedTables.push(table) | ||
| const kind = queryKind(projection) | ||
| const total = kind === 'count' | ||
| ? state.counts.get(table) ?? 0 | ||
| : kind === 'credits' | ||
| ? state.credits | ||
| : state.spend.get(table) ?? 0 | ||
| return { where: async () => [{ total: String(total) }] } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Model credit query scopes in the test double.
The stub returns state.credits for every credit query. It ignores the createdAt and billingMode predicates.
The paid usage tests can pass if getChatbotCreditUsage stops applying its monthly or platform-only filters. Add historical, platform, and BYOK credit rows. Assert that each plan counts only its required rows.
🤖 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 `@tests/unit/ai-budget-ledgers.test.ts` around lines 76 - 86, Update the db
stub used by the test double around queryKind so credit queries apply the
supplied createdAt and billingMode predicates instead of always returning
state.credits. Add representative historical, platform, and BYOK credit rows,
then assert getChatbotCreditUsage counts only the rows required by each paid
usage plan.
Add aria-pressed attribute to model buttons and reposition the selected indicator icon for better visual alignment.
- Remove the `shared/feature-flags` registry and associated composables and server utilities as the chatbot feature is now fully rolled out. - Update `marketing-redirect` middleware to handle authenticated vs. unauthenticated pricing page access, redirecting logged-in users directly to the billing dashboard. - Add unit tests for marketing redirect logic.
- Update demo modals and prompts to direct users to create an account. - Add "scheduled_first" and configurable sorting to interview queries. - Prevent demo account interactions in the chatbot.
Replace the previous credit-based model for Free workspaces with a fixed lifetime allowance of 20 chatbot prompts. Every prompt now consumes one unit regardless of token length or model choice. Paid plans retain their existing percentage-based credit meter.
FREE_PLAN_CHATBOT_TURN_LIMIT) for Free-tier workspaces to manage costs while allowing a functional demo.ChatbotQuotaUpsellCard.vueto transition users who have exhausted their free turns into the Solo plan.ai_usage_eventledger to record non-analysis AI spend and clarify usage for billing enforcement.Summary
Type of change
Validation
DCO
Signed-off-by) viagit commit -sSummary by CodeRabbit
New Features
Changed