feat(recommendations): validate product images via resource_check and allow removing products from approvals#6
Conversation
📝 WalkthroughWalkthroughThis PR implements multi-faceted feature enhancements across the approval and conversation systems, along with new infrastructure tooling. It adds a product removal flow to the approval detail modal with tracking state and backend mutations; extends conversation filtering to support priority, category, and search parameters; standardizes conversation type identifiers from kebab-case to snake_case (product-recommendation → product_recommendation, etc.) and introduces general and spam types; adds a resource_check Convex tool for validating remote resource accessibility via HTTP HEAD requests; integrates website page caching into the page content crawler with a cache-first lookup pattern; and refines TypeScript types for website page metadata from Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Areas requiring additional attention:
Possibly related PRs
Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro (Legacy)
⛔ Files ignored due to path filters (1)
services/platform/convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (25)
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx(4 hunks)services/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsx(6 hunks)services/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx(2 hunks)services/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsx(1 hunks)services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx(1 hunks)services/platform/app/(app)/dashboard/[id]/conversations/page.tsx(1 hunks)services/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.ts(2 hunks)services/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.ts(1 hunks)services/platform/convex/agent_tools/convex_tools/files/resource_check_tool.ts(1 hunks)services/platform/convex/agent_tools/tool_registry.ts(3 hunks)services/platform/convex/approvals.ts(1 hunks)services/platform/convex/conversations.ts(1 hunks)services/platform/convex/model/conversations/create_conversation_public.ts(1 hunks)services/platform/convex/model/conversations/get_conversations_page.ts(4 hunks)services/platform/convex/model/websites/bulk_upsert_pages.ts(1 hunks)services/platform/convex/model/websites/get_page_by_url.ts(1 hunks)services/platform/convex/model/websites/index.ts(1 hunks)services/platform/convex/model/websites/types.ts(2 hunks)services/platform/convex/predefined_workflows/general_product_recommendation.ts(2 hunks)services/platform/convex/schema.ts(1 hunks)services/platform/convex/websites.ts(2 hunks)services/platform/convex/workflow/actions/conversation/conversation_action.ts(2 hunks)services/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_email.ts(2 hunks)services/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.ts(2 hunks)services/platform/hooks/use-conversation-filters.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*
📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)
Use English only for ALL user-facing content including UI components, labels, buttons, dialogs, forms, toast messages, error messages, success messages, comments, documentation, README files, variable names, function names, and type names
Files:
services/platform/convex/model/conversations/create_conversation_public.tsservices/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_email.tsservices/platform/convex/conversations.tsservices/platform/convex/approvals.tsservices/platform/convex/model/websites/bulk_upsert_pages.tsservices/platform/hooks/use-conversation-filters.tsservices/platform/convex/model/websites/types.tsservices/platform/convex/schema.tsservices/platform/convex/model/websites/get_page_by_url.tsservices/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsxservices/platform/convex/model/conversations/get_conversations_page.tsservices/platform/convex/agent_tools/convex_tools/files/resource_check_tool.tsservices/platform/convex/agent_tools/tool_registry.tsservices/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsxservices/platform/convex/workflow/actions/conversation/conversation_action.tsservices/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.tsservices/platform/convex/websites.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsxservices/platform/convex/model/websites/index.tsservices/platform/convex/predefined_workflows/general_product_recommendation.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsxservices/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.tsservices/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.tsservices/platform/app/(app)/dashboard/[id]/conversations/page.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)
**/*.{ts,tsx,js,jsx}: Use Vercel AI SDK with OpenAI - import from 'ai' and '@ai-sdk/openai', never use raw OpenAI SDK or OpenRouter
Never hallucinate API keys - always use environment variables and existing .env configuration
Use camelCase for function names (e.g.,getUserData)
Use SCREAMING_SNAKE_CASE for constants (e.g.,API_BASE_URL,MAX_RETRIES)
Use feature flags with enums (TypeScript) or const objects (JavaScript) with UPPERCASE_WITH_UNDERSCORE naming
Implement error handling with try-catch pattern: check for result.error and display descriptive toast messages using result.error as title
Files:
services/platform/convex/model/conversations/create_conversation_public.tsservices/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_email.tsservices/platform/convex/conversations.tsservices/platform/convex/approvals.tsservices/platform/convex/model/websites/bulk_upsert_pages.tsservices/platform/hooks/use-conversation-filters.tsservices/platform/convex/model/websites/types.tsservices/platform/convex/schema.tsservices/platform/convex/model/websites/get_page_by_url.tsservices/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsxservices/platform/convex/model/conversations/get_conversations_page.tsservices/platform/convex/agent_tools/convex_tools/files/resource_check_tool.tsservices/platform/convex/agent_tools/tool_registry.tsservices/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsxservices/platform/convex/workflow/actions/conversation/conversation_action.tsservices/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.tsservices/platform/convex/websites.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsxservices/platform/convex/model/websites/index.tsservices/platform/convex/predefined_workflows/general_product_recommendation.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsxservices/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.tsservices/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.tsservices/platform/app/(app)/dashboard/[id]/conversations/page.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)
**/*.{ts,tsx}: Use kebab-case for file names (e.g.,user-profile.tsx)
Use PascalCase for component names (e.g.,UserProfile)
Use descriptive messages as toast title (never generic 'Error'), with optional description for additional context only
Follow component structure: 'use client' directive, imports, interface Props, hooks, effects, event handlers, then render
Prioritize data fetching methods in order: Server Actions (preferred), Route Handlers (when needed), Client-side (minimal use)
Use React.memo for expensive components to optimize performance
Use Next.js Image component for all images instead of native img tags
Use dynamic imports for code splitting
Files:
services/platform/convex/model/conversations/create_conversation_public.tsservices/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_email.tsservices/platform/convex/conversations.tsservices/platform/convex/approvals.tsservices/platform/convex/model/websites/bulk_upsert_pages.tsservices/platform/hooks/use-conversation-filters.tsservices/platform/convex/model/websites/types.tsservices/platform/convex/schema.tsservices/platform/convex/model/websites/get_page_by_url.tsservices/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsxservices/platform/convex/model/conversations/get_conversations_page.tsservices/platform/convex/agent_tools/convex_tools/files/resource_check_tool.tsservices/platform/convex/agent_tools/tool_registry.tsservices/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsxservices/platform/convex/workflow/actions/conversation/conversation_action.tsservices/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.tsservices/platform/convex/websites.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsxservices/platform/convex/model/websites/index.tsservices/platform/convex/predefined_workflows/general_product_recommendation.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsxservices/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.tsservices/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.tsservices/platform/app/(app)/dashboard/[id]/conversations/page.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx
services/*/convex/*.ts
📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)
Thin wrapper API modules (like
services/platform/convex/documents.ts) may export multiple Convex functions as wrappers that delegate to model helpers, but must not contain business logic and must only perform argument/return validation and delegation
Files:
services/platform/convex/conversations.tsservices/platform/convex/approvals.tsservices/platform/convex/schema.tsservices/platform/convex/websites.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (.cursor/rules/figma_rules.mdc)
**/*.{tsx,jsx}: Avoid specifyingfont-['Inter:Regular',_sans-serif]as it should be the default
Only specify font-family when using non-default fonts likefont-['Inter:Medium',_sans-serif]
Ensure font-family matches font-weight (Inter:Regular with font-normal, Inter:Medium with font-medium)
Useleading-normalinstead ofleading-[normal]in Tailwind classes
Use standard font size classes instead of arbitrary values:text-[12px]→text-xs,text-[14px]→text-sm,text-[16px]→text-base,text-[18px]→text-lg,text-[20px]→text-xl,text-[24px]→text-2xl
Use semantic spacing classes:p-[4px]→p-1,p-[8px]→p-2,m-[4px]→m-1,m-[8px]→m-2
Convert pixel values to rem using the 16px base for width and height measurements:w-[278px]→w-[17.375rem],h-[48px]→h-[3rem],min-w-[120px]→min-w-[7.5rem],max-w-[400px]→max-w-[25rem]
NEVER use hardcoded colors liketext-gray-500,bg-gray-100,border-gray-200; ALWAYS use design system semantic colors:text-foregroundfor primary text,text-muted-foregroundfor secondary text and icons,bg-backgroundfor main backgrounds,bg-mutedfor subtle backgrounds and hover states,border-borderfor borders
ALWAYS use the Table component instead of custom flex layouts; useTable,TableHeader,TableBody,TableRow,TableHead,TableCellcomponents with proper column widths using rem units and semantic colors
Files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsxservices/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsxservices/platform/app/(app)/dashboard/[id]/conversations/page.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx
**/app/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)
In Next.js App Router, use
page.tsxas server components by default; use'use client'only for interactions and state
Files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsxservices/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsxservices/platform/app/(app)/dashboard/[id]/conversations/page.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx
🧠 Learnings (51)
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Always use the new Convex function syntax with `query`, `mutation`, `internalQuery`, `internalMutation`, `action`, or `internalAction` with explicit `args`, `returns`, and `handler` properties
Applied to files:
services/platform/convex/conversations.tsservices/platform/convex/websites.ts
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : Always use the new Convex function syntax (query/mutation/action with args/returns/handler)
Applied to files:
services/platform/convex/conversations.tsservices/platform/convex/model/websites/bulk_upsert_pages.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx
📚 Learning: 2025-12-02T08:13:51.379Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:51.379Z
Learning: Applies to services/*/convex/*.ts : Thin wrapper API modules (like `services/platform/convex/documents.ts`) may export multiple Convex functions as wrappers that delegate to model helpers, but must not contain business logic and must only perform argument/return validation and delegation
Applied to files:
services/platform/convex/conversations.tsservices/platform/convex/agent_tools/tool_registry.ts
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : Use .unique() to fetch a single document and fail on multiple matches
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Use `.unique()` to get a single document from a query; this method throws an error if multiple documents match
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Use `.unique()` to get a single document from a query. This method throws an error if multiple documents match.
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Use the `paginationOptsValidator` from `convex/server` for paginated queries with `numItems` and `cursor` parameters. Queries ending in `.paginate()` return objects with `page`, `isDone`, and `continueCursor` properties.
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.tsservices/platform/convex/websites.ts
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : Define paginated queries using paginationOptsValidator and .paginate(opts)
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.tsservices/platform/convex/websites.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Thoughtfully organize files with public query, mutation, or action functions within the `convex/` directory following file-based routing conventions.
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.tsservices/platform/convex/websites.ts
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : Do not use .filter() in queries; define an index and use withIndex instead
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Try to use as few calls from actions to queries and mutations as possible to avoid race conditions, since queries and mutations are transactions
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Index fields must be queried in the same order they are defined; create separate indexes if you need to query by different field orderings
Applied to files:
services/platform/convex/model/websites/bulk_upsert_pages.ts
📚 Learning: 2025-08-21T15:02:40.745Z
Learnt from: CR
Repo: talecorp/lanserhof PR: 0
File: .cursor/rules/posthog-integration.mdc:0-0
Timestamp: 2025-08-21T15:02:40.745Z
Learning: Applies to **/*.{ts,tsx} : In TypeScript, store feature flag names in an enum
Applied to files:
services/platform/hooks/use-conversation-filters.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.ts : Ensure TypeScript Record key/value types match validators (e.g., Record<Id<'users'>, string>)
Applied to files:
services/platform/convex/model/websites/types.tsservices/platform/convex/schema.tsservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : Use the documented Convex validators for all supported types (e.g., v.id, v.int64, v.number, v.boolean, v.string, v.bytes, v.array, v.object, v.record)
Applied to files:
services/platform/convex/model/websites/types.tsservices/platform/convex/schema.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Use `v.id(tableName)` validator for document IDs, and use strict TypeScript types with `Id<'tableName'>` instead of generic string types for function arguments and returns
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : When defining a `Record` type, correctly provide the type of the key and value. For example, `v.record(v.id('users'), v.string())` would have type `Record<Id<'users'>, string>`.
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Use `v.record()` for defining a record type. `v.map()` and `v.set()` are not supported.
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Be strict with types, particularly around document ids. If a function takes in an id for a document in the 'users' table, take in `Id<'users'>` rather than `string`.
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Be strict with types, particularly around IDs of documents; use `Id<'tableName'>` rather than `string` for function arguments and returns
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/schema.{ts,tsx} : Always define your schema in `convex/schema.ts` and import schema definition functions from `convex/server`.
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/schema.ts : Define discriminated unions in schema with v.union and v.literal for the discriminator
Applied to files:
services/platform/convex/schema.ts
📚 Learning: 2025-07-03T08:43:49.346Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/next-best-practice.mdc:0-0
Timestamp: 2025-07-03T08:43:49.346Z
Learning: Applies to **/*.{tsx,jsx} : Use Next.js Image component for images
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-08-21T14:59:36.874Z
Learnt from: CR
Repo: talecorp/lanserhof PR: 0
File: .cursorrules:0-0
Timestamp: 2025-08-21T14:59:36.874Z
Learning: Applies to {app,components}/**/*.{tsx,jsx} : Use Next.js Image component for all images
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-07-19T15:29:09.401Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursorrules:0-0
Timestamp: 2025-07-19T15:29:09.401Z
Learning: Applies to app/components/**/*.{js,jsx,ts,tsx} : Use Next.js Image component for all images.
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-10-11T11:46:02.452Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursorrules:0-0
Timestamp: 2025-10-11T11:46:02.452Z
Learning: Applies to **/*.{tsx,jsx} : Use Next.js Image component for all images instead of <img>
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-11-30T12:29:39.745Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-11-30T12:29:39.745Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use Next.js `Image` component for all images instead of native `<img>` tags
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-12-02T08:13:51.379Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:51.379Z
Learning: Applies to **/*.{ts,tsx} : Use Next.js Image component for all images instead of native img tags
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-07-19T15:30:00.886Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/core-rules.mdc:0-0
Timestamp: 2025-07-19T15:30:00.886Z
Learning: Applies to **/*.{tsx,jsx} : Use memoization (e.g., useMemo) for expensive operations and cache repeated calculations (e.g., useRef with Map) in React components
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-07-03T08:43:49.346Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/next-best-practice.mdc:0-0
Timestamp: 2025-07-03T08:43:49.346Z
Learning: Applies to **/*.{tsx,jsx} : Use dynamic imports for code splitting
Applied to files:
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : For async iteration over queries, prefer for await...of instead of .collect() or .take(n)
Applied to files:
services/platform/convex/model/conversations/get_conversations_page.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : When using async iteration on queries, use `for await (const row of query)` syntax instead of `.collect()` or `.take(n)`
Applied to files:
services/platform/convex/model/conversations/get_conversations_page.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : When using async iteration, don't use `.collect()` or `.take(n)` on query results. Instead, use the `for await (const row of query)` syntax.
Applied to files:
services/platform/convex/model/conversations/get_conversations_page.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Always include argument and return validators for all Convex functions (query, internalQuery, mutation, internalMutation, action, internalAction)
Applied to files:
services/platform/convex/websites.ts
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Use the `internal` object from `convex/_generated/api.ts` to call internal functions registered with `internalQuery`, `internalMutation`, or `internalAction`
Applied to files:
services/platform/convex/websites.ts
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : Register internal functions with internalQuery, internalMutation, and internalAction (imported from ./_generated/server)
Applied to files:
services/platform/convex/websites.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register internal (private) functions that can only be called by other Convex functions and are not exposed to the public API.
Applied to files:
services/platform/convex/websites.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions, including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`. If a function doesn't return anything, include `returns: v.null()`.
Applied to files:
services/platform/convex/websites.ts
📚 Learning: 2025-07-19T15:30:00.886Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/core-rules.mdc:0-0
Timestamp: 2025-07-19T15:30:00.886Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use enums (TypeScript) or const objects (JavaScript) for feature flags
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsx
📚 Learning: 2025-10-11T11:46:02.452Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursorrules:0-0
Timestamp: 2025-10-11T11:46:02.452Z
Learning: Applies to **/*.{ts,tsx,js} : Maintain type safety throughout the codebase
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to **/*.{ts,tsx} : When using Record type, declare records with explicit key/value types (const rec: Record<K, V> = {...})
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-08-21T15:02:40.745Z
Learnt from: CR
Repo: talecorp/lanserhof PR: 0
File: .cursor/rules/posthog-integration.mdc:0-0
Timestamp: 2025-08-21T15:02:40.745Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use a consistent naming convention for feature flag containers; enum/const object members must be UPPERCASE_WITH_UNDERSCORE
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-07-03T08:43:49.346Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/next-best-practice.mdc:0-0
Timestamp: 2025-07-03T08:43:49.346Z
Learning: Applies to **/*.{ts,tsx} : Define proper types using TypeScript interfaces and types
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-08-21T14:59:36.874Z
Learnt from: CR
Repo: talecorp/lanserhof PR: 0
File: .cursorrules:0-0
Timestamp: 2025-08-21T14:59:36.874Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Feature flags must use enums (TS) or const objects (JS), named UPPERCASE_WITH_UNDERSCORE, and used in minimal locations
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-07-20T08:40:33.623Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/posthog-integration.mdc:0-0
Timestamp: 2025-07-20T08:40:33.623Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : If using TypeScript, use an enum to store flag names. If using JavaScript, store flag names as strings to an object declared as a constant, to simulate an enum. Use a consistent naming convention for this storage. enum/const object members should be written UPPERCASE_WITH_UNDERSCORE.
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Always use `as const` for string literals in discriminated union types.
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx
📚 Learning: 2024-12-16T21:19:20.116Z
Learnt from: Israeltheminer
Repo: talecorp/dev PR: 113
File: apps/app/app/[locale]/(protected)/(user-portal)/merchant/activate-account/onboarding/business-type-dialog.tsx:33-36
Timestamp: 2024-12-16T21:19:20.116Z
Learning: In `business-type-dialog.tsx`, the application is currently targeting a single country where `'GOVERNMENT_ENTITY'` is not supported, so filtering out `'GOVERNMENT_ENTITY'` directly in the UI component is acceptable.
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsxservices/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsx
📚 Learning: 2025-12-02T08:13:24.267Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.267Z
Learning: Applies to convex/crons.{ts,tsx} : If a cron calls an internal function, always import the `internal` object from '_generated/api', even if the internal function is registered in the same file.
Applied to files:
services/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.ts
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/crons.ts : If a cron calls an internal function, import internal from ./_generated/api even if the function is in the same file
Applied to files:
services/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.ts
📚 Learning: 2025-12-02T08:13:24.266Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.266Z
Learning: Applies to convex/**/*.{ts,tsx} : Try to use as few calls from actions to queries and mutations as possible, as they are transactions and splitting logic introduces race condition risks.
Applied to files:
services/platform/app/(app)/dashboard/[id]/conversations/page.tsx
🧬 Code graph analysis (10)
services/platform/convex/model/conversations/get_conversations_page.ts (1)
services/platform/convex/model/conversations/transform_conversation.ts (1)
transformConversation(14-136)
services/platform/convex/agent_tools/convex_tools/files/resource_check_tool.ts (1)
services/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.ts (2)
ResourceCheckResult(10-20)checkResourceAccessible(22-122)
services/platform/convex/agent_tools/tool_registry.ts (1)
services/platform/convex/agent_tools/convex_tools/files/resource_check_tool.ts (1)
resourceCheckTool(14-63)
services/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsx (1)
services/platform/convex/approvals.ts (1)
removeRecommendedProduct(186-196)
services/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.ts (1)
services/platform/convex/lib/debug_log.ts (1)
createDebugLog(27-39)
services/platform/convex/websites.ts (1)
services/platform/convex/model/websites/types.ts (1)
websitePageValidator(40-52)
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx (2)
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsx (1)
TypeFilter(19-19)services/platform/hooks/use-conversation-filters.ts (1)
TypeFilter(4-9)
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsx (1)
services/platform/hooks/use-conversation-filters.ts (1)
TypeFilter(4-9)
services/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.ts (1)
services/platform/convex/agent_tools/convex_tools/crawler/helpers/get_crawler_service_url.ts (1)
getCrawlerServiceUrl(7-15)
services/platform/app/(app)/dashboard/[id]/conversations/page.tsx (1)
services/rag/app/services/cognee_service.py (1)
search(503-593)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build services/platform
🔇 Additional comments (28)
services/platform/convex/model/websites/types.ts (1)
50-51: LGTM: Improved type specificity for metadata fields.The validator change from
v.any()tov.record(v.string(), v.any())provides better type safety by explicitly defining these fields as JSON-like objects with string keys, which aligns with typical metadata and structured data usage patterns.services/platform/convex/model/websites/bulk_upsert_pages.ts (1)
35-35: LGTM: Correct use of.unique()for single document retrieval.The change from
.first()to.unique()aligns with Convex best practices. This method will throw an error if multiple documents match the query, providing better data integrity enforcement.Based on learnings,
.unique()is the correct method to fetch a single document from a query.services/platform/convex/model/websites/index.ts (1)
47-48: LGTM: Clean export additions.The new exports for
getPageByUrlandGetPageByUrlArgsfollow the established pattern in this index file and properly expose the new page lookup functionality.services/platform/convex/schema.ts (1)
773-774: LGTM: Improved schema specificity for website page fields.The schema change from
v.any()tov.record(v.string(), v.any())provides better type constraints for metadata and structured data fields, ensuring they are JSON-like objects with string keys. This aligns with the corresponding validator changes intypes.ts.services/platform/convex/websites.ts (1)
199-212: LGTM: Well-structured internal query for page lookup.The new
getWebsitePageByUrlInternalquery follows Convex best practices:
- Properly uses
internalQuerywith explicitargsandreturnsvalidators- Returns validated union type with
websitePageValidator- Correctly delegates to the model layer
- Follows the established pattern of other internal queries in this file
services/platform/convex/agent_tools/convex_tools/crawler/helpers/fetch_page_content.ts (1)
34-89: LGTM: Well-implemented cache-first optimization.The cache lookup implementation demonstrates good practices:
- Gracefully degrades when
organizationIdis unavailable- Uses proper error handling with try-catch to prevent cache failures from breaking the fetch flow
- Applies consistent truncation logic (MAX_CONTENT_CHARS) for both cached and fetched content
- Includes comprehensive debug logging for cache hits, misses, and errors
- Returns early on cache hit to avoid unnecessary external service calls
- Maintains existing behavior as fallback
This optimization should improve performance for frequently accessed pages while preserving reliability.
services/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsx (3)
42-44: LGTM!The state declaration correctly tracks which product is currently being removed.
128-130: LGTM!The mutation hook correctly references the new backend mutation.
667-668: LGTM!The new props are consistently passed to
ApprovalDetailModalacross all conditional render branches, enabling the modal to trigger removal and reflect removal state correctly.Also applies to: 776-777, 804-805
services/platform/convex/approvals.ts (1)
183-196: LGTM!The mutation follows the thin wrapper pattern correctly: delegates to the model layer, uses RLS for access control, and maintains consistency with other public mutations in this file.
services/platform/app/(app)/dashboard/[id]/approvals/components/approval-detail-modal.tsx (5)
42-43: LGTM!The new optional props are correctly typed and integrated into the component signature.
Also applies to: 54-55
83-86: LGTM!The handler appropriately guards against missing prop and delegates to the parent handler.
95-95: LGTM!The slice correctly limits visible products to the first three highest-confidence recommendations.
180-198: ProductCard component correctly implements removal props with proper safety checks.The component properly handles
canRemove,onRemove, andisRemovingprops:
- Removal button only renders when both
canRemoveandonRemoveare truthy (line 105)- Calls
onRemove(product.id)on click (line 109)- Shows loading spinner when
isRemovingis true (lines 113-114)- Button is disabled during removal (line 110)
The implementation is safe and follows the expected pattern.
63-65: No changes needed. The code correctly requiresorganizationIdbecause it is a non-optional property in theApprovalDetailtype definition. WhenapprovalDetail?.customer.emailis truthy,approvalDetail.organizationIdis guaranteed to exist, so the query will not skip unexpectedly.services/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.ts (2)
49-65: LGTM!Timeout handling with
AbortControlleris correctly implemented, with cleanup in both success and error paths. The fetch configuration is appropriate for resource checking.
98-121: LGTM!Error handling is comprehensive and properly structured. The function safely extracts error messages and returns a consistent failure result.
services/platform/convex/agent_tools/tool_registry.ts (1)
21-21: LGTM!The
resource_checktool is correctly registered in bothTOOL_NAMESandTOOL_REGISTRYarrays with consistent naming following the snake_case convention used by other tools.Also applies to: 37-37, 63-63
services/platform/convex/predefined_workflows/general_product_recommendation.ts (1)
210-210: LGTM!The
resource_checktool is correctly added to the available tools array, enabling image URL validation during recommendation generation.services/platform/convex/agent_tools/convex_tools/files/resource_check_tool.ts (1)
14-63: LGTM!The
resource_checktool is well-implemented with:
- Clear, comprehensive description including usage examples
- Proper Zod schema validation for arguments
- Correct handler delegation to the helper function with proper parameter mapping (
timeout_ms→timeoutMs)- Type-safe definition using
as const satisfies ToolDefinitionservices/platform/app/(app)/dashboard/[id]/conversations/components/conversations.tsx (2)
30-35: LGTM! Improved type formatting.The reformatting of the
SelectionStatediscriminated union improves readability with no functional changes.
54-70: LGTM! Filter parameters correctly threaded through to query.The new
search,category, andpriorityparameters are properly extracted from URL search params and passed to the Convex query, enabling server-side filtering.services/platform/app/(app)/dashboard/[id]/conversations/components/filter-status-indicator.tsx (1)
22-28: LGTM! Improved type safety with strongly typed Record.The
typeLabelsdeclaration now usesRecord<TypeFilter, string>for better type safety, ensuring all TypeFilter values have corresponding labels. The snake_case keys align with the updated TypeFilter type definition.Based on learnings, this follows the best practice of using explicit key/value types with Record.
services/platform/convex/workflow/actions/conversation/conversation_action.ts (1)
243-243: LGTM! Type parameter correctly propagated to helpers.The
typeparameter is properly threaded through to bothcreateConversationFromEmailandcreateConversationFromSentEmailhelper calls, maintaining consistency with the action's parameter definition and validation.Also applies to: 266-266
services/platform/app/(app)/dashboard/[id]/conversations/page.tsx (1)
52-54: Backend correctly handles optional filter parameters.The query properly accepts optional
priority,category, andsearchparameters withv.optional()validators. The backend implementation safely checks if these parameters exist before processing them, splitting comma-separated values into Sets for efficient filtering or converting search terms to lowercase. The frontend's conditional logic on lines 52-54 correctly passesundefinedfor empty strings, which Convex naturally handles as absent optional parameters.services/platform/convex/conversations.ts (1)
421-423: LGTM! Clean extension of filter parameters.The addition of
priority,category, andsearchparameters follows the thin wrapper pattern correctly. The implementation properly validates arguments and delegates to the model layer without introducing business logic.Based on coding guidelines for thin wrapper API modules.
services/platform/convex/model/conversations/get_conversations_page.ts (1)
40-92: LGTM! Efficient and correct filtering implementation.The filtering logic is well-implemented with:
- Set-based lookups for O(1) priority and category filtering
- Precomputed helpers to avoid repeated processing
- Case-insensitive search with safe null handling via optional chaining
- Comma-separated value support for flexible filtering
- Proper use of
for await...offor async iterationBased on learnings about async iteration patterns.
services/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_email.ts (1)
41-41: LGTM! New optional type parameter with sensible default.The addition of the optional
typeparameter allows conversation classification, with a sensible default of'general'. This implementation is consistent with the schema definition and mirrors the pattern used increate_conversation_public.ts. The change preserves existing behavior while enabling new functionality.
| const handleRemoveRecommendation = async ( | ||
| approvalId: string, | ||
| productId: string, | ||
| ) => { | ||
| if (!memberContext?.member?._id) { | ||
| toast({ | ||
| title: 'You must be logged in to update recommendations.', | ||
| variant: 'destructive', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| setRemovingProductId(productId); | ||
| try { | ||
| await removeRecommendedProduct({ | ||
| approvalId: approvalId as Id<'approvals'>, | ||
| productId, | ||
| }); | ||
| } catch (error) { | ||
| console.error('Failed to remove recommendation:', error); | ||
| toast({ | ||
| title: 'Failed to remove recommendation. Please try again.', | ||
| variant: 'destructive', | ||
| }); | ||
| } finally { | ||
| setRemovingProductId(null); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for removeRecommendedProduct implementation in the backend
# First, find the structure of convex directory
find services/platform/convex -type f -name "*.ts" | head -20Repository: tale-project/tale
Length of output: 1598
🏁 Script executed:
#!/bin/bash
# Search for approvals-related files
# Find all approvals files
fd -t f -i approvals services/platform/convex
# Also search for the removeRecommendedProduct function across convex
rg -t ts "removeRecommendedProduct" services/platform/convexRepository: tale-project/tale
Length of output: 984
🏁 Script executed:
#!/bin/bash
# Read the backend implementation
echo "=== remove_recommended_product.ts implementation ==="
cat -n services/platform/convex/model/approvals/remove_recommended_product.ts
echo -e "\n=== approvals.ts mutation wrapper ==="
rg -A 20 "export const removeRecommendedProduct = mutationWithRLS" services/platform/convex/approvals.tsRepository: tale-project/tale
Length of output: 2055
Backend lacks validation that productId exists before removal.
The removeRecommendedProduct implementation filters out matching products without verifying the productId exists in the recommendedProducts array first. This causes silent failures where removal appears successful even if the product was never in the list, leading to inconsistent state between frontend expectations and backend reality.
Add an explicit check to ensure the productId exists before removal:
// Verify product exists in the list
const productExists = recommendedProducts.some((product) => product['productId'] === args.productId);
if (!productExists) {
throw new Error('Product not found in recommendations');
}🤖 Prompt for AI Agents
In services/platform/app/(app)/dashboard/[id]/approvals/components/approvals.tsx
around lines 195 to 222, the frontend calls removeRecommendedProduct but the
backend can silently succeed when the passed productId isn't in
recommendedProducts; add a pre-removal existence check in the backend: verify
recommendedProducts contains an entry with productId === args.productId, and if
not, throw a descriptive error (e.g., "Product not found in recommendations") so
the frontend sees a failure instead of a silent no-op; ensure the thrown error
is propagated to the caller so the existing catch/toast handles it.
| id: 'product_recommendation' as TypeFilter, | ||
| label: 'Product recommendation', | ||
| }, | ||
| { id: 'service-request' as TypeFilter, label: 'Service request' }, | ||
| { id: 'churn-survey' as TypeFilter, label: 'Churn survey' }, | ||
| { id: 'service_request' as TypeFilter, label: 'Service request' }, | ||
| { id: 'churn_survey' as TypeFilter, label: 'Churn survey' }, | ||
| { id: 'general' as TypeFilter, label: 'General' }, | ||
| { id: 'spam' as TypeFilter, label: 'Spam' }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check for kebab-case filter values in the codebase
rg -nP --type=ts --type=tsx '(product-recommendation|service-request|churn-survey)' -C3Repository: tale-project/tale
Length of output: 88
🏁 Script executed:
# Look at the filter-dropdown.tsx file to understand the full context
cat -n services/platform/app/\(app\)/dashboard/\[id\]/conversations/components/filter-dropdown.tsx | head -60Repository: tale-project/tale
Length of output: 2181
🏁 Script executed:
# Find and examine use-conversation-filters.ts
fd -t f 'use-conversation-filters' --type=tsRepository: tale-project/tale
Length of output: 230
🏁 Script executed:
# Search for any filter ID references to understand how they're used
rg -nP --type=ts --type=tsx 'product_recommendation|service_request|churn_survey' -C2 | head -50Repository: tale-project/tale
Length of output: 88
🏁 Script executed:
# Check for URL parameter parsing related to filters
rg -nP --type=ts --type=tsx 'searchParams|filter.*get|URLSearchParams' services/platform/app/\(app\)/dashboard/\[id\]/conversations -C3Repository: tale-project/tale
Length of output: 88
🏁 Script executed:
# Find the use-conversation-filters hook file
find . -type f -name '*use-conversation-filters*' 2>/dev/nullRepository: tale-project/tale
Length of output: 114
🏁 Script executed:
# Search for kebab-case filter values - simpler approach
rg 'product-recommendation|service-request|churn-survey'Repository: tale-project/tale
Length of output: 1798
🏁 Script executed:
# Search for snake_case filter usage patterns
rg 'product_recommendation|service_request|churn_survey' -C3 | head -80Repository: tale-project/tale
Length of output: 8652
🏁 Script executed:
# Look for URL parameter handling in conversations directory
rg 'searchParams|URLSearchParams' services/platform/app/\(app\)/dashboard/\[id\]/conversations/ -C3Repository: tale-project/tale
Length of output: 6489
🏁 Script executed:
# Check for any filter mapping or conversion logic
rg 'filter.*map|convert.*filter|kebab|snake' services/platform/app/\(app\)/dashboard/\[id\]/conversations/Repository: tale-project/tale
Length of output: 881
🏁 Script executed:
# Look at conversations-list.tsx to see the mapping
cat -n services/platform/app/\(app\)/dashboard/\[id\]/conversations/components/conversations-list.tsx | head -100Repository: tale-project/tale
Length of output: 4181
🏁 Script executed:
# Get more context on the mapping
rg "product-recommendation|service-request|churn-survey" services/platform/app/\(app\)/dashboard/\[id\]/conversations/components/conversations-list.tsx -B5 -A5Repository: tale-project/tale
Length of output: 371
🏁 Script executed:
# Check the use-conversation-filters hook for filter handling
cat -n services/platform/hooks/use-conversation-filters.tsRepository: tale-project/tale
Length of output: 8110
🏁 Script executed:
# Search for where the category parameter is validated/used
rg 'category.*filter|isValidTypeFilter|TypeFilter' services/platform/hooks/use-conversation-filters.ts -A3 -B3Repository: tale-project/tale
Length of output: 1169
🏁 Script executed:
# Search for tests related to filter parsing
find . -type f -name '*.test.ts' -o -name '*.test.tsx' -o -name '*.spec.ts' | xargs grep -l "filter\|category" 2>/dev/null | head -10Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Check for any migration or breaking change documentation
rg 'breaking.*change|migration|backward.*compat' services/platform/hooks/ -lRepository: tale-project/tale
Length of output: 112
🏁 Script executed:
# Verify if kebab-case would actually cause issues by checking actual filtering
rg 'category.*filter|TypeFilter.*includes' services/platform -A5 -B2Repository: tale-project/tale
Length of output: 3187
🏁 Script executed:
# Check if there's any conversion happening between formats
rg 'kebab|snake|replace.*-.*_|split.*-' services/platform/app/\(app\)/dashboard/\[id\]/conversations/Repository: tale-project/tale
Length of output: 43
Implement backward-compatible URL parameter parsing for filter identifiers.
The filter option IDs changed from kebab-case ('product-recommendation') to snake_case ('product_recommendation'), creating a breaking change. Existing bookmarks, saved URLs, and shared links using kebab-case format will silently fail—the validation in use-conversation-filters.ts (lines 49-60) only accepts exact snake_case matches, so kebab-case values won't pass the includes() check.
Add a conversion utility in use-conversation-filters.ts to normalize kebab-case values to snake_case during URL parameter parsing, enabling both formats during a transition period:
function normalizeFilterValue(value: string): string {
return value.replace(/-/g, '_');
}Then update the category parsing logic (lines 46-61) to convert incoming values before validation.
🤖 Prompt for AI Agents
In
services/platform/app/(app)/dashboard/[id]/conversations/components/filter-dropdown.tsx
around lines 29 to 35 the filter IDs were switched from kebab-case to snake_case
which breaks existing URLs; add backward-compatible parsing in
use-conversation-filters.ts by creating a small normalizeFilterValue function
that replaces '-' with '_' and use it when reading the category URL param
(convert the incoming value via normalizeFilterValue before running the
includes() validation) so both kebab-case and snake_case are accepted.
| const trimmedUrl = url.trim(); | ||
| const lower = trimmedUrl.toLowerCase(); | ||
|
|
||
| if (!lower.startsWith('http://') && !lower.startsWith('https://')) { | ||
| const result: ResourceCheckResult = { | ||
| success: false, | ||
| url: trimmedUrl, | ||
| finalUrl: trimmedUrl, | ||
| status: 0, | ||
| ok: false, | ||
| contentType: null, | ||
| contentLength: null, | ||
| isImage: false, | ||
| error: 'Only http and https URLs are supported', | ||
| }; | ||
|
|
||
| debugLog('tool:resource_check invalid scheme', { | ||
| url: trimmedUrl, | ||
| }); | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding URL structure validation.
The current validation only checks the URL scheme. Consider using the URL constructor to validate the URL structure before attempting the fetch, which would provide clearer error messages for malformed URLs.
Apply this diff to add URL validation:
export async function checkResourceAccessible({
url,
timeoutMs = 10_000,
}: ResourceCheckArgs): Promise<ResourceCheckResult> {
const trimmedUrl = url.trim();
const lower = trimmedUrl.toLowerCase();
if (!lower.startsWith('http://') && !lower.startsWith('https://')) {
const result: ResourceCheckResult = {
success: false,
url: trimmedUrl,
finalUrl: trimmedUrl,
status: 0,
ok: false,
contentType: null,
contentLength: null,
isImage: false,
error: 'Only http and https URLs are supported',
};
debugLog('tool:resource_check invalid scheme', {
url: trimmedUrl,
});
return result;
}
+
+ // Validate URL structure
+ try {
+ new URL(trimmedUrl);
+ } catch {
+ const result: ResourceCheckResult = {
+ success: false,
+ url: trimmedUrl,
+ finalUrl: trimmedUrl,
+ status: 0,
+ ok: false,
+ contentType: null,
+ contentLength: null,
+ isImage: false,
+ error: 'Invalid URL format',
+ };
+
+ debugLog('tool:resource_check invalid URL format', {
+ url: trimmedUrl,
+ });
+
+ return result;
+ }| const contentType = response.headers.get('content-type'); | ||
| const contentLengthHeader = response.headers.get('content-length'); | ||
| const parsedLength = contentLengthHeader | ||
| ? Number.parseInt(contentLengthHeader, 10) | ||
| : Number.NaN; | ||
| const contentLength = Number.isFinite(parsedLength) ? parsedLength : null; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider rejecting negative content-length values.
While unlikely from legitimate servers, Number.parseInt will successfully parse negative numbers. Consider adding a check to ensure contentLength is non-negative.
const contentType = response.headers.get('content-type');
const contentLengthHeader = response.headers.get('content-length');
const parsedLength = contentLengthHeader
? Number.parseInt(contentLengthHeader, 10)
: Number.NaN;
- const contentLength = Number.isFinite(parsedLength) ? parsedLength : null;
+ const contentLength = Number.isFinite(parsedLength) && parsedLength >= 0 ? parsedLength : null;🤖 Prompt for AI Agents
In
services/platform/convex/agent_tools/convex_tools/files/helpers/check_resource_accessible.ts
around lines 66 to 71, the code currently parses the Content-Length header but
does not guard against negative values; update the logic so that after parsing
you verify parsedLength is a finite, non-negative integer (e.g., parsedLength >=
0) before assigning it to contentLength, otherwise set contentLength to null (or
handle as an invalid header) to avoid treating negative lengths as valid.
| status: (args.status as any) || 'open', | ||
| priority: args.priority, | ||
| type: args.type || 'General', | ||
| type: args.type || 'general', |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file in question to understand context
cat -n services/platform/convex/model/conversations/create_conversation_public.tsRepository: tale-project/tale
Length of output: 1273
🏁 Script executed:
# Search for hardcoded 'General' (capitalized) references in TypeScript files
rg -nP --type=ts "type\s*:\s*['\"]General['\"]" -C 3Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Look for migration or schema files that might reference conversation types
fd -e ts -e js -e sql | xargs rg -l "conversation.*type\|type.*conversation" | head -20Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Search for all references to 'General' in the codebase (broader search)
rg -nP "General" --type=ts --type=js -C 2 | head -50Repository: tale-project/tale
Length of output: 5202
🏁 Script executed:
# Check for any database seed files or fixtures with 'General'
fd \( -name "*seed*" -o -name "*fixture*" -o -name "*mock*" \) -type f | xargs rg -l "General" 2>/dev/null || echo "No seed/fixture files found with 'General'"Repository: tale-project/tale
Length of output: 935
🏁 Script executed:
# Check the full transform_conversation.ts file to understand its purpose
cat -n services/platform/convex/model/conversations/transform_conversation.ts | head -130Repository: tale-project/tale
Length of output: 5683
🏁 Script executed:
# Search for how conversation type is queried or filtered (case-sensitive checks)
rg -n "conversation\.type\|conversationType" --type=ts --type=tsx -C 2Repository: tale-project/tale
Length of output: 88
🏁 Script executed:
# Check for schema definitions or type enums
fd \( -name "schema*" -o -name "*dataModel*" \) -type f | xargs catRepository: tale-project/tale
Length of output: 291
🏁 Script executed:
# Look for any case-insensitive comparison logic
rg -n "toLowerCase\|toUpperCase\|case.?insensitive" --type=ts --type=tsx -C 2 | grep -i "type" -A 3 -B 3Repository: tale-project/tale
Length of output: 88
🏁 Script executed:
# Find and read the dataModel schema
find . -name "*dataModel*" -type f | head -5Repository: tale-project/tale
Length of output: 175
🏁 Script executed:
# Check for convex migrations
find . -path "*/convex/migrations*" -o -path "*/convex/*migration*" -type fRepository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Search for TODO or FIXME comments related to case normalization or conversation type
rg -n "TODO|FIXME" --type=ts -C 2 | grep -i "case\|general\|type\|normaliz" -B 2 -A 2Repository: tale-project/tale
Length of output: 1565
🏁 Script executed:
# Check the types file to see what ConversationItem expects
cat -n services/platform/convex/model/conversations/types.tsRepository: tale-project/tale
Length of output: 9501
🏁 Script executed:
# Check for any queries that filter by conversation type to see if case matters
rg -n "type.*general\|type.*General" --type=ts -i -B 2 -A 2 | grep -v "node_modules" | head -40Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Look at the schema definition to understand the conversations table
cat -n services/platform/convex/_generated/dataModel.d.ts | grep -A 30 "conversations"Repository: tale-project/tale
Length of output: 43
Update transform_conversation.ts to use lowercase 'general' for consistency.
The default value in createConversationPublic.ts (line 29) changed to lowercase 'general', but transform_conversation.ts (line 115) still defaults to capitalized 'General'. This inconsistency means new conversations will use 'general' while existing/transformed conversations default to 'General', causing type mismatches. Update line 115 to:
type: conversation.type || 'general',
This ensures consistent lowercase defaults across both creation and transformation paths.
🤖 Prompt for AI Agents
In services/platform/convex/model/conversations/transform_conversation.ts around
line 115, the default conversation type is set to capitalized 'General' which
conflicts with create_conversation_public.ts (line 29) using lowercase
'general'; update the default at line 115 to use lowercase 'general' so both
creation and transformation paths use the same default value.
| metadata?: Record<string, unknown>; | ||
| structuredData?: Record<string, unknown>; |
There was a problem hiding this comment.
Type mismatch: TypeScript type should align with validator.
The TypeScript types use Record<string, unknown> but the corresponding validator at lines 50-51 uses v.record(v.string(), v.any()), which corresponds to Record<string, any> in TypeScript.
Based on learnings, TypeScript Record key/value types must match validators to ensure type consistency.
Apply this diff to align the TypeScript types with the validators:
- metadata?: Record<string, unknown>;
- structuredData?: Record<string, unknown>;
+ metadata?: Record<string, any>;
+ structuredData?: Record<string, any>;🤖 Prompt for AI Agents
In services/platform/convex/model/websites/types.ts around lines 114-115 the
declared types metadata?: Record<string, unknown> and structuredData?:
Record<string, unknown> do not match the validator v.record(v.string(), v.any())
which corresponds to Record<string, any>; update those two type annotations to
metadata?: Record<string, any> and structuredData?: Record<string, any> so the
TypeScript types align with the validator contract.
| value: `You are a general product recommendation engine for this business. | ||
|
|
||
| You receive the full customer record (all fields and metadata) and should propose products that are relevant, valuable, and timely for this customer. | ||
|
|
||
| You can call tools to look up products and product details. Prefer using existing product relationship metadata when available, but you may also browse the catalog. | ||
|
|
||
| STRICT JSON RULES (CRITICAL): | ||
| - You MUST return ONLY a single JSON object. | ||
| - Do NOT include any natural language before or after the JSON. | ||
| - Do NOT wrap the JSON in markdown (for example: no markdown code fences or code blocks). | ||
| - Do NOT say things like "Here is the JSON". | ||
| - The response MUST be directly parseable by JSON.parse. | ||
| - All keys MUST be in double quotes. | ||
| - Strings MUST use double quotes, never single quotes. | ||
| - Do NOT include comments inside the JSON. | ||
|
|
||
| If you are unsure, be conservative and still return a best-effort JSON object in the exact format requested. | ||
|
|
||
| If tools are available, call them as needed to look up products or search the knowledge base before you produce the final JSON response. | ||
| `, | ||
|
|
||
| You receive the full customer record (all fields and metadata) and should propose products that are relevant, valuable, and timely for this customer. | ||
|
|
||
| You can call tools to look up products and product details. Prefer using existing product relationship metadata when available, but you may also browse the catalog. | ||
|
|
||
| CRITICAL BUSINESS RULE: | ||
| - You must ONLY recommend products that are currently active and have available stock strictly greater than 0. | ||
| - Never recommend products that are inactive, archived, or have stock less than or equal to 0. | ||
| - Use tools as needed to verify that a product is active and in stock before including it in the recommendations. | ||
|
|
||
| IMAGE URL VALIDATION RULES (CRITICAL): | ||
| - Recommended products must have a valid, accessible image URL. | ||
| - Use the "resource_check" tool to validate each candidate product's image URL before including it in the final recommendations. | ||
| - Only include a product if resource_check returns success: true AND isImage: true for the chosen image URL. | ||
| - If you cannot find a valid image URL for a product that passes resource_check, do NOT include that product in the recommendations. | ||
|
|
||
| STRICT JSON RULES (CRITICAL): | ||
| - You MUST return ONLY a single JSON object. | ||
| - Do NOT include any natural language before or after the JSON. | ||
| - Do NOT wrap the JSON in markdown (for example: no markdown code fences or code blocks). | ||
| - Do NOT say things like "Here is the JSON". | ||
| - The response MUST be directly parseable by JSON.parse. | ||
| - All keys MUST be in double quotes. | ||
| - Strings MUST use double quotes, never single quotes. | ||
| - Do NOT include comments inside the JSON. | ||
|
|
||
| If you are unsure, be conservative and still return a best-effort JSON object in the exact format requested. | ||
|
|
||
| If tools are available, call them as needed to look up products, validate image URLs, or search the knowledge base before you produce the final JSON response. | ||
| `, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider improving prompt formatting and reducing verbosity.
The system prompt contains inconsistent indentation (mixed tabs/spaces) that makes it harder to read and maintain. Additionally, the prompt is quite verbose with repetitive sections about JSON formatting rules.
Consider:
- Using consistent indentation (spaces only)
- Condensing repetitive JSON formatting rules into a more concise section
- Structuring the prompt with clear sections (Business Rules, Output Format, etc.)
Example refactor:
{
name: 'llmSystemPrompt',
- value: `You are a general product recommendation engine for this business.
-
- You receive the full customer record (all fields and metadata) and should propose products that are relevant, valuable, and timely for this customer.
-
- You can call tools to look up products and product details. Prefer using existing product relationship metadata when available, but you may also browse the catalog.
-
- CRITICAL BUSINESS RULE:
- - You must ONLY recommend products that are currently active and have available stock strictly greater than 0.
- - Never recommend products that are inactive, archived, or have stock less than or equal to 0.
- - Use tools as needed to verify that a product is active and in stock before including it in the recommendations.
-
- IMAGE URL VALIDATION RULES (CRITICAL):
- - Recommended products must have a valid, accessible image URL.
- - Use the "resource_check" tool to validate each candidate product's image URL before including it in the final recommendations.
- - Only include a product if resource_check returns success: true AND isImage: true for the chosen image URL.
- - If you cannot find a valid image URL for a product that passes resource_check, do NOT include that product in the recommendations.
-
- STRICT JSON RULES (CRITICAL):
- - You MUST return ONLY a single JSON object.
- - Do NOT include any natural language before or after the JSON.
- - Do NOT wrap the JSON in markdown (for example: no markdown code fences or code blocks).
- - Do NOT say things like "Here is the JSON".
- - The response MUST be directly parseable by JSON.parse.
- - All keys MUST be in double quotes.
- - Strings MUST use double quotes, never single quotes.
- - Do NOT include comments inside the JSON.
-
- If you are unsure, be conservative and still return a best-effort JSON object in the exact format requested.
-
- If tools are available, call them as needed to look up products, validate image URLs, or search the knowledge base before you produce the final JSON response.
- `,
+ value: `You are a general product recommendation engine for this business.
+
+You receive the full customer record (all fields and metadata) and should propose products that are relevant, valuable, and timely for this customer.
+
+You can call tools to look up products and product details. Prefer using existing product relationship metadata when available, but you may also browse the catalog.
+
+CRITICAL BUSINESS RULES:
+- ONLY recommend products that are currently active and have stock > 0
+- Use tools to verify product status and stock before including in recommendations
+- Validate image URLs using "resource_check" tool (must return success: true AND isImage: true)
+- Skip products without valid, accessible image URLs
+
+OUTPUT FORMAT:
+Return ONLY valid JSON (no markdown, no explanations) directly parseable by JSON.parse.
+Use the exact structure shown in the user prompt.
+
+Use available tools (product_read, rag_search, resource_check) as needed before generating the final response.`,
},📝 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.
| value: `You are a general product recommendation engine for this business. | |
| You receive the full customer record (all fields and metadata) and should propose products that are relevant, valuable, and timely for this customer. | |
| You can call tools to look up products and product details. Prefer using existing product relationship metadata when available, but you may also browse the catalog. | |
| STRICT JSON RULES (CRITICAL): | |
| - You MUST return ONLY a single JSON object. | |
| - Do NOT include any natural language before or after the JSON. | |
| - Do NOT wrap the JSON in markdown (for example: no markdown code fences or code blocks). | |
| - Do NOT say things like "Here is the JSON". | |
| - The response MUST be directly parseable by JSON.parse. | |
| - All keys MUST be in double quotes. | |
| - Strings MUST use double quotes, never single quotes. | |
| - Do NOT include comments inside the JSON. | |
| If you are unsure, be conservative and still return a best-effort JSON object in the exact format requested. | |
| If tools are available, call them as needed to look up products or search the knowledge base before you produce the final JSON response. | |
| `, | |
| You receive the full customer record (all fields and metadata) and should propose products that are relevant, valuable, and timely for this customer. | |
| You can call tools to look up products and product details. Prefer using existing product relationship metadata when available, but you may also browse the catalog. | |
| CRITICAL BUSINESS RULE: | |
| - You must ONLY recommend products that are currently active and have available stock strictly greater than 0. | |
| - Never recommend products that are inactive, archived, or have stock less than or equal to 0. | |
| - Use tools as needed to verify that a product is active and in stock before including it in the recommendations. | |
| IMAGE URL VALIDATION RULES (CRITICAL): | |
| - Recommended products must have a valid, accessible image URL. | |
| - Use the "resource_check" tool to validate each candidate product's image URL before including it in the final recommendations. | |
| - Only include a product if resource_check returns success: true AND isImage: true for the chosen image URL. | |
| - If you cannot find a valid image URL for a product that passes resource_check, do NOT include that product in the recommendations. | |
| STRICT JSON RULES (CRITICAL): | |
| - You MUST return ONLY a single JSON object. | |
| - Do NOT include any natural language before or after the JSON. | |
| - Do NOT wrap the JSON in markdown (for example: no markdown code fences or code blocks). | |
| - Do NOT say things like "Here is the JSON". | |
| - The response MUST be directly parseable by JSON.parse. | |
| - All keys MUST be in double quotes. | |
| - Strings MUST use double quotes, never single quotes. | |
| - Do NOT include comments inside the JSON. | |
| If you are unsure, be conservative and still return a best-effort JSON object in the exact format requested. | |
| If tools are available, call them as needed to look up products, validate image URLs, or search the knowledge base before you produce the final JSON response. | |
| `, | |
| name: 'llmSystemPrompt', | |
| value: `You are a general product recommendation engine for this business. | |
| You receive the full customer record (all fields and metadata) and should propose products that are relevant, valuable, and timely for this customer. | |
| You can call tools to look up products and product details. Prefer using existing product relationship metadata when available, but you may also browse the catalog. | |
| CRITICAL BUSINESS RULES: | |
| - ONLY recommend products that are currently active and have stock > 0 | |
| - Use tools to verify product status and stock before including in recommendations | |
| - Validate image URLs using "resource_check" tool (must return success: true AND isImage: true) | |
| - Skip products without valid, accessible image URLs | |
| OUTPUT FORMAT: | |
| Return ONLY valid JSON (no markdown, no explanations) directly parseable by JSON.parse. | |
| Use the exact structure shown in the user prompt. | |
| Use available tools (product_read, rag_search, resource_check) as needed before generating the final response.`, |
| { | ||
| "productId": "CONVEX_PRODUCT_ID", | ||
| "productName": "Product Name", | ||
| "imageUrl": "Valid, accessible image URL for the product that has been verified using the resource_check tool. If you cannot find such an image URL for a product, do not include that product in the recommendations.", |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Refactor the overly long imageUrl field description.
Line 162 contains an extremely long description embedded directly in the JSON example structure, which severely impacts readability and breaks the clean structure of the example.
Move the detailed explanation outside the JSON example:
value: `Customer record (full JSON):
{{currentCustomer}}
- Task: Recommend up to 5 products that this customer is most likely to benefit from next.
-
- You must ONLY recommend products that are active and have stock greater than 0 (strictly positive). Do not recommend any products that are inactive, archived, or out of stock.
-
- Recommended products MUST also have a valid, accessible image URL. For each candidate product, you MUST:
- - Identify a candidate image URL (for example from the product's imageUrl field).
- - Call the "resource_check" tool with that URL.
- - Only include the product in the final recommendations if resource_check indicates the URL is accessible and isImage is true.
- - If you cannot find a valid image URL that passes resource_check for a product, skip that product and choose another one.
-
- Use any available product relationship metadata (for example on products they are linked to) when relevant, and you may also browse the product catalog and RAG knowledge base.
+Task: Recommend up to 5 products that this customer is most likely to benefit from next.
+
+Requirements:
+- ONLY recommend active products with stock > 0
+- Each product MUST have a valid image URL verified via resource_check (success: true AND isImage: true)
+- Skip products that fail image validation
+- Use product relationship metadata and browse catalog/RAG as needed
- You MUST return strictly and ONLY a JSON object in this shape (no extra text):
+Return ONLY a JSON object in this exact shape:
{
"recommendations": [
{
"productId": "CONVEX_PRODUCT_ID",
"productName": "Product Name",
- "imageUrl": "Valid, accessible image URL for the product that has been verified using the resource_check tool. If you cannot find such an image URL for a product, do not include that product in the recommendations.",
+ "imageUrl": "Verified image URL",
"relationshipType": "Complementary|Upgrade|Bundle|Substitute|Other",
"reasoning": "Why this product is recommended for this customer",
"confidence": 0.82
}
],
"summary": "Short summary of your overall recommendation strategy"
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
services/platform/convex/predefined_workflows/general_product_recommendation.ts
around line 162, the JSON example embeds an overly long description for the
"imageUrl" field which hurts readability; refactor by replacing the long inline
sentence with a short placeholder (e.g., "Valid image URL") inside the JSON and
move the detailed explanation into a brief comment or separate explanatory
paragraph immediately above or below the JSON example so the JSON stays clean
while the full guidance remains available.
| priority?: string; | ||
| accountEmail?: string; // The mailbox address of the account/mailbox being synced | ||
| providerId?: Id<'emailProviders'>; | ||
| type?: string; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file to understand the context
wc -l services/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.tsRepository: tale-project/tale
Length of output: 166
🏁 Script executed:
# Read the relevant sections of the file
cat -n services/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.ts | head -40Repository: tale-project/tale
Length of output: 1969
🏁 Script executed:
# Also check around line 286
cat -n services/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.ts | sed -n '275,300p'Repository: tale-project/tale
Length of output: 1184
🏁 Script executed:
# Search for existing type values in conversation-related files
rg -nP --type=ts -C3 '\btype\s*:\s*["\'](?!general|product_recommendation|service_request|churn_survey|spam)' services/platform/convex/workflow/actions/conversation/Repository: tale-project/tale
Length of output: 314
🏁 Script executed:
# Look for database schema or migrations related to conversation types
fd -e ts -e js -e json -e sql -e graphql . services/platform/convex | grep -i "schema\|migration\|type" | head -20Repository: tale-project/tale
Length of output: 1199
🏁 Script executed:
# Search for historical conversation type patterns
rg -n 'type.*["\']' services/platform/convex/workflow/actions/conversation/ --type=ts | grep -i "general\|General" | head -20Repository: tale-project/tale
Length of output: 129
🏁 Script executed:
# Check the conversation types file
cat -n services/platform/convex/model/conversations/types.tsRepository: tale-project/tale
Length of output: 9501
🏁 Script executed:
# Search for type assignments in conversation-related files
rg 'type.*:' services/platform/convex/model/conversations/ -A1 -B1Repository: tale-project/tale
Length of output: 2890
🏁 Script executed:
# Look for createConversationWithMessage mutation to understand expected type values
rg 'createConversationWithMessage' services/platform/convex/ -lRepository: tale-project/tale
Length of output: 436
🏁 Script executed:
# Search for any references to conversation type values
rg 'general|product_recommendation|service_request|churn_survey' services/platform/convex/model/conversations/Repository: tale-project/tale
Length of output: 169
🏁 Script executed:
# Check if there are any database migrations or fixtures with type values
fd -e ts -e js services/platform/convex | xargs grep -l 'type.*general\|type.*product_recommendation' | head -10Repository: tale-project/tale
Length of output: 462
🏁 Script executed:
# Read the transform_conversation.ts file to understand when 'General' is used
cat -n services/platform/convex/model/conversations/transform_conversation.ts | head -50Repository: tale-project/tale
Length of output: 2248
🏁 Script executed:
# Search for all places where conversation.type is set or defaulted
rg 'type.*\|\|' services/platform/convex/model/conversations/ -B2 -A2Repository: tale-project/tale
Length of output: 1716
🏁 Script executed:
# Look for any TypeFilter references in the codebase
rg 'TypeFilter' services/platform/convex/ -lRepository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Check if there are any other conversation type values being used
rg "type.*:.*['\"]" services/platform/convex/model/conversations/ --type=tsRepository: tale-project/tale
Length of output: 282
🏁 Script executed:
# Examine create_conversation_with_message.ts to see if it has any validation
cat -n services/platform/convex/model/conversations/create_conversation_with_message.ts | head -80Repository: tale-project/tale
Length of output: 3322
🏁 Script executed:
# Check when transform_conversation.ts uses 'General' and what data actually exists
rg "conversation\.type" services/platform/convex/model/conversations/ -B3 -A3Repository: tale-project/tale
Length of output: 1612
🏁 Script executed:
# See if there are any migrations or data fixes related to type casing
rg -i "general" services/platform/convex/model/conversations/ -B2 -A2Repository: tale-project/tale
Length of output: 1177
🏁 Script executed:
# Check the create_conversation.ts to see what it stores vs transform
cat -n services/platform/convex/model/conversations/create_conversation.ts | head -100Repository: tale-project/tale
Length of output: 1010
🏁 Script executed:
# Look for any existing conversations being queried to see if they have type values
rg "\.type" services/platform/convex/model/conversations/ --type=ts | grep -v "contentType\|messageType\|resourceType\|approvalType\|eventType"Repository: tale-project/tale
Length of output: 762
Type parameter addition is consistent with the codebase, but note the casing inconsistency in the conversation type system.
The optional type parameter with lowercase 'general' default aligns with create_conversation_public.ts. However, transform_conversation.ts defaults to capital 'General' when the stored type is null. While this creates inconsistency between the storage and display layers, it doesn't require a data migration since:
- No
TypeFilterenum exists to enforce specific values—the field accepts any string - Transform logic handles null values gracefully
- Existing conversations will continue to work regardless of casing
For consistency, consider aligning transform_conversation.ts to also use lowercase 'general'.
🤖 Prompt for AI Agents
In
services/platform/convex/workflow/actions/conversation/helpers/create_conversation_from_sent_email.ts
around line 26 there is an optional type param using lowercase 'general' which
conflicts with transform_conversation.ts's default 'General'; update
transform_conversation.ts to use lowercase 'general' as the default conversation
type to keep storage and transform layers consistent, then run tests/linter and
adjust any tests or typings that assume the capitalized form.
| export type TypeFilter = | ||
| | 'product-recommendation' | ||
| | 'service-request' | ||
| | 'churn-survey'; | ||
| | 'product_recommendation' | ||
| | 'service_request' | ||
| | 'churn_survey' | ||
| | 'general' | ||
| | 'spam'; |
There was a problem hiding this comment.
Add backward compatibility for kebab-case URL parameters.
The TypeFilter migration from kebab-case to snake_case is a breaking change that will silently ignore URL parameters using the old format (e.g., ?category=product-recommendation). Users with bookmarks or saved links will lose their filter selections.
Add backward-compatible parsing to handle both formats during a transition period:
// Handle multiple category filters (comma-separated)
const types: TypeFilter[] = [];
if (categoryParam) {
const categoryValues = categoryParam.split(',');
categoryValues.forEach((value) => {
+ // Map old kebab-case to new snake_case for backward compatibility
+ const normalizedValue = value
+ .replace('product-recommendation', 'product_recommendation')
+ .replace('service-request', 'service_request')
+ .replace('churn-survey', 'churn_survey');
+
if (
[
'product_recommendation',
'service_request',
'churn_survey',
'general',
'spam',
- ].includes(value)
+ ].includes(normalizedValue)
) {
- types.push(value as TypeFilter);
+ types.push(normalizedValue as TypeFilter);
}
});
}After a deprecation period, remove the mapping and add a console warning for users still using the old format.
Also applies to: 50-56
🤖 Prompt for AI Agents
In services/platform/hooks/use-conversation-filters.ts around lines 4 to 9 (and
similarly apply for lines 50 to 56), the TypeFilter union was changed from
kebab-case to snake_case which breaks handling of URL params like
?category=product-recommendation; update the URL-parsing logic to accept both
formats by mapping known kebab-case values to their snake_case equivalents
before converting to TypeFilter, emit a single console.warn when an incoming
kebab-case value is detected (so users see a deprecation notice), and keep the
mapping logic for the agreed transition period (remove the mapping and replace
the warn with nothing after deprecation).
…lidate - repairObject now recursively processes array elements to repair corrupted keys inside nested objects within arrays - validateObject now recursively validates array elements to catch control characters in nested object keys - Added biome-ignore comments for intentional control character regex patterns - Added camelCase normalization for repaired field names (e.g., userprompt -> userPrompt) Addresses CodeRabbit review comments #5, #6, and #7. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…lidate - repairObject now recursively processes array elements to repair corrupted keys inside nested objects within arrays - validateObject now recursively validates array elements to catch control characters in nested object keys - Added biome-ignore comments for intentional control character regex patterns - Added camelCase normalization for repaired field names (e.g., userprompt -> userPrompt) Addresses CodeRabbit review comments #5, #6, and #7. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… allow removing products from approvals (#6)
Three round-2-confirmed gaps in the dual-control flow. placeLegalHold (W1 #11) - targetType: 'org' is the nuclear "halt all retention" hold; a single admin placing it freezes cleanup org-wide, defeating dual-control. Refuse by default; deployments running a single admin opt back in via the existing TALE_LEGAL_HOLD_SINGLE_ADMIN_OK env. A future maker- checker placement flow (legalHoldPlaceRequests + approveLegalHoldPlacement) supersedes this; today's commit restores the contract. approveLegalHoldRelease (W1 #12) - 5-min minimum delay between request and approval (constant RELEASE_APPROVAL_MIN_DELAY_MS). Defeats the chained-call attack where one admin requests + a second admin instantly approves via the same automation flow. Skipped when self-approval is in effect (the escape hatch already opted out of dual-control entirely). - Re-check that the original requester is still an org admin at approval time. A demoted/removed requester whose request lingered loses the ability to retroactively gate a destructive change. closeLegalMatter (W1 #10 + W4 #6) - Implements the docstring promise (round-2 v08 bonus): closing a matter now fans out PENDING release requests for every active hold linked via matterRef, leveraging the new by_organizationId_matterRef index. Approval still requires a second admin via approveLegalHoldRelease — matter-close does NOT auto-release, dual-control survives. - Returns releaseRequestsFiled count so the UI can confirm the cascade. Audit row records linkedHolds + releaseRequestsFiled. - Skips holds with an existing pending or approved request (idempotent against re-running close on a half-closed matter).
…eout Round-2 v15 confirmed: /config unauthenticated, /openapi.json + /docs + /redoc unauthenticated, RAG container ran as root, default token baked into image ENV, strict-mode env name diverged across the wire, non-constant-time token compare, plus three SSRF-guard gaps. services/rag/app/auth.py - W7 #3: hmac.compare_digest replaces == on the bearer compare. Removes the dead-code EXEMPT_PATHS frozenset. services/rag/app/routers/health.py - W7 #1: split into public_router (`/`, `/health`) and protected_router (`/config`). main.py mounts the protected one under Depends(verify_internal_token). Old `router` re-export stays for backwards compat. services/rag/app/main.py - W7 #2: docs_url / redoc_url / openapi_url are None outside debug. - W7 #4: CORS allow_credentials flipped to False (bearer rides Authorization, never cookies). - W7 #1 wiring: mount health-public + health-protected separately. services/rag/app/config.py - W7 #8: require_custom_internal_token accepts BOTH RAG_REQUIRE_CUSTOM_INTERNAL_TOKEN and TALE_REQUIRE_CUSTOM_RAG_TOKEN via pydantic AliasChoices. services/rag/Dockerfile + services/convex/Dockerfile - W7 #5: RAG container runs as non-root (uid:gid 1001:1001 `app`). RAG ingests untrusted PDFs/DOCX through native parsers; biggest blast radius in the stack, now hardened. - W7 #6: removed RAG_INTERNAL_TOKEN=tale-rag-dev-only ENV bake from both runtime + scratch-squash stages and the matching bake in services/convex/Dockerfile. Operators MUST supply via env / compose / k8s secret. services/platform/convex/lib/helpers/rag_config.ts - W7 #9 F1: `redirect: 'manual'` on every ragFetch. - W7 #9 F2: added fc00::/7 (IPv6 ULA) to v6 blocklist (AWS IPv6 IMDSv2). - W7 #9 F3: strip trailing `.` before hostname blocklist lookup. - W7 #9 F4: re-validate URL per ragFetch invocation (DNS rebinding + env rotation mitigation). - W7 #9 F9: deleted path.startsWith('http') override branch (future- bypass foot-gun). services/platform/convex/agent_tools/rag/helpers/fetch_document_chunks.ts - W7 #10: pass timeoutMs=60_000 (default 10s was a regression). - Plus MAX_ITERATIONS=30 cap and "cursor did not advance" break to defend against an adversarial RAG response.
Round-2 v09 confirmed: schemas + validators + restoreChatThread all
documented a two-pass grace window (active → expired → cascaded after
graceDays), but the cleanup runner only ever ran a single pass that
cascaded ANY thread whose updatedAt was past the cutoff — including
freshly-trashed threads (no grace window honored) and even active
threads whose user just hadn't messaged in N days. The 'expired'
literal in the validator union was dead — nothing wrote it.
This commit wires the two-pass model the docstrings already promised:
internal_queries.ts
- listExpiredThreads now filters status === 'active' (or unset legacy
rows). Pass-A input.
- New listGraceExpiredThreads filters status IN ('trashed', 'expired')
AND statusChangedAt < graceCutoffMs. Pass-B input.
internal_mutations_retention.ts
- New markThreadExpired internal mutation: idempotent flip from
'active' to 'expired' with statusChangedAt = now. Carries the same
hold-aware + TOCTOU + cross-org guard as the cascade mutations.
Audit subtype: chat_history.retention_expired.
retention_cleanup.cleanupChatHistory
- graceDays === 0 (default): single-pass, cascade active expired
threads directly. Matches pre-Bundle-3 behavior — no trash UX.
- graceDays > 0: Pass A flips active expired threads to 'expired',
giving operators a graceDays-long window to restore via
restoreChatThread. Pass B in the same run cascades trashed/expired
threads whose statusChangedAt is older than the grace cutoff.
Per-thread hold checks fire on BOTH passes; held threads are skipped
with an info log. Existing restoreChatThread already supports both
'trashed' and 'expired' (admin-only for the latter), so the loop is
closed end to end.
Out-of-scope here:
- list_trashed_threads.ts UI wiring (W1 #11 / list_trashed_threads
rewrite — covered separately).
- getThreadStatus carve-out so a bookmarked URL of a trashed thread
renders a restore prompt instead of 404 (W5 #6 — UI follow-up).
Three round-2-confirmed gaps in the dual-control flow. placeLegalHold (W1 #11) - targetType: 'org' is the nuclear "halt all retention" hold; a single admin placing it freezes cleanup org-wide, defeating dual-control. Refuse by default; deployments running a single admin opt back in via the existing TALE_LEGAL_HOLD_SINGLE_ADMIN_OK env. A future maker- checker placement flow (legalHoldPlaceRequests + approveLegalHoldPlacement) supersedes this; today's commit restores the contract. approveLegalHoldRelease (W1 #12) - 5-min minimum delay between request and approval (constant RELEASE_APPROVAL_MIN_DELAY_MS). Defeats the chained-call attack where one admin requests + a second admin instantly approves via the same automation flow. Skipped when self-approval is in effect (the escape hatch already opted out of dual-control entirely). - Re-check that the original requester is still an org admin at approval time. A demoted/removed requester whose request lingered loses the ability to retroactively gate a destructive change. closeLegalMatter (W1 #10 + W4 #6) - Implements the docstring promise (round-2 v08 bonus): closing a matter now fans out PENDING release requests for every active hold linked via matterRef, leveraging the new by_organizationId_matterRef index. Approval still requires a second admin via approveLegalHoldRelease — matter-close does NOT auto-release, dual-control survives. - Returns releaseRequestsFiled count so the UI can confirm the cascade. Audit row records linkedHolds + releaseRequestsFiled. - Skips holds with an existing pending or approved request (idempotent against re-running close on a half-closed matter).
…eout Round-2 v15 confirmed: /config unauthenticated, /openapi.json + /docs + /redoc unauthenticated, RAG container ran as root, default token baked into image ENV, strict-mode env name diverged across the wire, non-constant-time token compare, plus three SSRF-guard gaps. services/rag/app/auth.py - W7 #3: hmac.compare_digest replaces == on the bearer compare. Removes the dead-code EXEMPT_PATHS frozenset. services/rag/app/routers/health.py - W7 #1: split into public_router (`/`, `/health`) and protected_router (`/config`). main.py mounts the protected one under Depends(verify_internal_token). Old `router` re-export stays for backwards compat. services/rag/app/main.py - W7 #2: docs_url / redoc_url / openapi_url are None outside debug. - W7 #4: CORS allow_credentials flipped to False (bearer rides Authorization, never cookies). - W7 #1 wiring: mount health-public + health-protected separately. services/rag/app/config.py - W7 #8: require_custom_internal_token accepts BOTH RAG_REQUIRE_CUSTOM_INTERNAL_TOKEN and TALE_REQUIRE_CUSTOM_RAG_TOKEN via pydantic AliasChoices. services/rag/Dockerfile + services/convex/Dockerfile - W7 #5: RAG container runs as non-root (uid:gid 1001:1001 `app`). RAG ingests untrusted PDFs/DOCX through native parsers; biggest blast radius in the stack, now hardened. - W7 #6: removed RAG_INTERNAL_TOKEN=tale-rag-dev-only ENV bake from both runtime + scratch-squash stages and the matching bake in services/convex/Dockerfile. Operators MUST supply via env / compose / k8s secret. services/platform/convex/lib/helpers/rag_config.ts - W7 #9 F1: `redirect: 'manual'` on every ragFetch. - W7 #9 F2: added fc00::/7 (IPv6 ULA) to v6 blocklist (AWS IPv6 IMDSv2). - W7 #9 F3: strip trailing `.` before hostname blocklist lookup. - W7 #9 F4: re-validate URL per ragFetch invocation (DNS rebinding + env rotation mitigation). - W7 #9 F9: deleted path.startsWith('http') override branch (future- bypass foot-gun). services/platform/convex/agent_tools/rag/helpers/fetch_document_chunks.ts - W7 #10: pass timeoutMs=60_000 (default 10s was a regression). - Plus MAX_ITERATIONS=30 cap and "cursor did not advance" break to defend against an adversarial RAG response.
Round-2 v09 confirmed: schemas + validators + restoreChatThread all
documented a two-pass grace window (active → expired → cascaded after
graceDays), but the cleanup runner only ever ran a single pass that
cascaded ANY thread whose updatedAt was past the cutoff — including
freshly-trashed threads (no grace window honored) and even active
threads whose user just hadn't messaged in N days. The 'expired'
literal in the validator union was dead — nothing wrote it.
This commit wires the two-pass model the docstrings already promised:
internal_queries.ts
- listExpiredThreads now filters status === 'active' (or unset legacy
rows). Pass-A input.
- New listGraceExpiredThreads filters status IN ('trashed', 'expired')
AND statusChangedAt < graceCutoffMs. Pass-B input.
internal_mutations_retention.ts
- New markThreadExpired internal mutation: idempotent flip from
'active' to 'expired' with statusChangedAt = now. Carries the same
hold-aware + TOCTOU + cross-org guard as the cascade mutations.
Audit subtype: chat_history.retention_expired.
retention_cleanup.cleanupChatHistory
- graceDays === 0 (default): single-pass, cascade active expired
threads directly. Matches pre-Bundle-3 behavior — no trash UX.
- graceDays > 0: Pass A flips active expired threads to 'expired',
giving operators a graceDays-long window to restore via
restoreChatThread. Pass B in the same run cascades trashed/expired
threads whose statusChangedAt is older than the grace cutoff.
Per-thread hold checks fire on BOTH passes; held threads are skipped
with an info log. Existing restoreChatThread already supports both
'trashed' and 'expired' (admin-only for the latter), so the loop is
closed end to end.
Out-of-scope here:
- list_trashed_threads.ts UI wiring (W1 #11 / list_trashed_threads
rewrite — covered separately).
- getThreadStatus carve-out so a bookmarked URL of a trashed thread
renders a restore prompt instead of 404 (W5 #6 — UI follow-up).
Three round-2-confirmed gaps in the dual-control flow. placeLegalHold (W1 #11) - targetType: 'org' is the nuclear "halt all retention" hold; a single admin placing it freezes cleanup org-wide, defeating dual-control. Refuse by default; deployments running a single admin opt back in via the existing TALE_LEGAL_HOLD_SINGLE_ADMIN_OK env. A future maker- checker placement flow (legalHoldPlaceRequests + approveLegalHoldPlacement) supersedes this; today's commit restores the contract. approveLegalHoldRelease (W1 #12) - 5-min minimum delay between request and approval (constant RELEASE_APPROVAL_MIN_DELAY_MS). Defeats the chained-call attack where one admin requests + a second admin instantly approves via the same automation flow. Skipped when self-approval is in effect (the escape hatch already opted out of dual-control entirely). - Re-check that the original requester is still an org admin at approval time. A demoted/removed requester whose request lingered loses the ability to retroactively gate a destructive change. closeLegalMatter (W1 #10 + W4 #6) - Implements the docstring promise (round-2 v08 bonus): closing a matter now fans out PENDING release requests for every active hold linked via matterRef, leveraging the new by_organizationId_matterRef index. Approval still requires a second admin via approveLegalHoldRelease — matter-close does NOT auto-release, dual-control survives. - Returns releaseRequestsFiled count so the UI can confirm the cascade. Audit row records linkedHolds + releaseRequestsFiled. - Skips holds with an existing pending or approved request (idempotent against re-running close on a half-closed matter).
…eout Round-2 v15 confirmed: /config unauthenticated, /openapi.json + /docs + /redoc unauthenticated, RAG container ran as root, default token baked into image ENV, strict-mode env name diverged across the wire, non-constant-time token compare, plus three SSRF-guard gaps. services/rag/app/auth.py - W7 #3: hmac.compare_digest replaces == on the bearer compare. Removes the dead-code EXEMPT_PATHS frozenset. services/rag/app/routers/health.py - W7 #1: split into public_router (`/`, `/health`) and protected_router (`/config`). main.py mounts the protected one under Depends(verify_internal_token). Old `router` re-export stays for backwards compat. services/rag/app/main.py - W7 #2: docs_url / redoc_url / openapi_url are None outside debug. - W7 #4: CORS allow_credentials flipped to False (bearer rides Authorization, never cookies). - W7 #1 wiring: mount health-public + health-protected separately. services/rag/app/config.py - W7 #8: require_custom_internal_token accepts BOTH RAG_REQUIRE_CUSTOM_INTERNAL_TOKEN and TALE_REQUIRE_CUSTOM_RAG_TOKEN via pydantic AliasChoices. services/rag/Dockerfile + services/convex/Dockerfile - W7 #5: RAG container runs as non-root (uid:gid 1001:1001 `app`). RAG ingests untrusted PDFs/DOCX through native parsers; biggest blast radius in the stack, now hardened. - W7 #6: removed RAG_INTERNAL_TOKEN=tale-rag-dev-only ENV bake from both runtime + scratch-squash stages and the matching bake in services/convex/Dockerfile. Operators MUST supply via env / compose / k8s secret. services/platform/convex/lib/helpers/rag_config.ts - W7 #9 F1: `redirect: 'manual'` on every ragFetch. - W7 #9 F2: added fc00::/7 (IPv6 ULA) to v6 blocklist (AWS IPv6 IMDSv2). - W7 #9 F3: strip trailing `.` before hostname blocklist lookup. - W7 #9 F4: re-validate URL per ragFetch invocation (DNS rebinding + env rotation mitigation). - W7 #9 F9: deleted path.startsWith('http') override branch (future- bypass foot-gun). services/platform/convex/agent_tools/rag/helpers/fetch_document_chunks.ts - W7 #10: pass timeoutMs=60_000 (default 10s was a regression). - Plus MAX_ITERATIONS=30 cap and "cursor did not advance" break to defend against an adversarial RAG response.
Round-2 v09 confirmed: schemas + validators + restoreChatThread all
documented a two-pass grace window (active → expired → cascaded after
graceDays), but the cleanup runner only ever ran a single pass that
cascaded ANY thread whose updatedAt was past the cutoff — including
freshly-trashed threads (no grace window honored) and even active
threads whose user just hadn't messaged in N days. The 'expired'
literal in the validator union was dead — nothing wrote it.
This commit wires the two-pass model the docstrings already promised:
internal_queries.ts
- listExpiredThreads now filters status === 'active' (or unset legacy
rows). Pass-A input.
- New listGraceExpiredThreads filters status IN ('trashed', 'expired')
AND statusChangedAt < graceCutoffMs. Pass-B input.
internal_mutations_retention.ts
- New markThreadExpired internal mutation: idempotent flip from
'active' to 'expired' with statusChangedAt = now. Carries the same
hold-aware + TOCTOU + cross-org guard as the cascade mutations.
Audit subtype: chat_history.retention_expired.
retention_cleanup.cleanupChatHistory
- graceDays === 0 (default): single-pass, cascade active expired
threads directly. Matches pre-Bundle-3 behavior — no trash UX.
- graceDays > 0: Pass A flips active expired threads to 'expired',
giving operators a graceDays-long window to restore via
restoreChatThread. Pass B in the same run cascades trashed/expired
threads whose statusChangedAt is older than the grace cutoff.
Per-thread hold checks fire on BOTH passes; held threads are skipped
with an info log. Existing restoreChatThread already supports both
'trashed' and 'expired' (admin-only for the latter), so the loop is
closed end to end.
Out-of-scope here:
- list_trashed_threads.ts UI wiring (W1 #11 / list_trashed_threads
rewrite — covered separately).
- getThreadStatus carve-out so a bookmarked URL of a trashed thread
renders a restore prompt instead of 404 (W5 #6 — UI follow-up).
Three round-2-confirmed gaps in the dual-control flow. placeLegalHold (W1 #11) - targetType: 'org' is the nuclear "halt all retention" hold; a single admin placing it freezes cleanup org-wide, defeating dual-control. Refuse by default; deployments running a single admin opt back in via the existing TALE_LEGAL_HOLD_SINGLE_ADMIN_OK env. A future maker- checker placement flow (legalHoldPlaceRequests + approveLegalHoldPlacement) supersedes this; today's commit restores the contract. approveLegalHoldRelease (W1 #12) - 5-min minimum delay between request and approval (constant RELEASE_APPROVAL_MIN_DELAY_MS). Defeats the chained-call attack where one admin requests + a second admin instantly approves via the same automation flow. Skipped when self-approval is in effect (the escape hatch already opted out of dual-control entirely). - Re-check that the original requester is still an org admin at approval time. A demoted/removed requester whose request lingered loses the ability to retroactively gate a destructive change. closeLegalMatter (W1 #10 + W4 #6) - Implements the docstring promise (round-2 v08 bonus): closing a matter now fans out PENDING release requests for every active hold linked via matterRef, leveraging the new by_organizationId_matterRef index. Approval still requires a second admin via approveLegalHoldRelease — matter-close does NOT auto-release, dual-control survives. - Returns releaseRequestsFiled count so the UI can confirm the cascade. Audit row records linkedHolds + releaseRequestsFiled. - Skips holds with an existing pending or approved request (idempotent against re-running close on a half-closed matter).
…eout Round-2 v15 confirmed: /config unauthenticated, /openapi.json + /docs + /redoc unauthenticated, RAG container ran as root, default token baked into image ENV, strict-mode env name diverged across the wire, non-constant-time token compare, plus three SSRF-guard gaps. services/rag/app/auth.py - W7 #3: hmac.compare_digest replaces == on the bearer compare. Removes the dead-code EXEMPT_PATHS frozenset. services/rag/app/routers/health.py - W7 #1: split into public_router (`/`, `/health`) and protected_router (`/config`). main.py mounts the protected one under Depends(verify_internal_token). Old `router` re-export stays for backwards compat. services/rag/app/main.py - W7 #2: docs_url / redoc_url / openapi_url are None outside debug. - W7 #4: CORS allow_credentials flipped to False (bearer rides Authorization, never cookies). - W7 #1 wiring: mount health-public + health-protected separately. services/rag/app/config.py - W7 #8: require_custom_internal_token accepts BOTH RAG_REQUIRE_CUSTOM_INTERNAL_TOKEN and TALE_REQUIRE_CUSTOM_RAG_TOKEN via pydantic AliasChoices. services/rag/Dockerfile + services/convex/Dockerfile - W7 #5: RAG container runs as non-root (uid:gid 1001:1001 `app`). RAG ingests untrusted PDFs/DOCX through native parsers; biggest blast radius in the stack, now hardened. - W7 #6: removed RAG_INTERNAL_TOKEN=tale-rag-dev-only ENV bake from both runtime + scratch-squash stages and the matching bake in services/convex/Dockerfile. Operators MUST supply via env / compose / k8s secret. services/platform/convex/lib/helpers/rag_config.ts - W7 #9 F1: `redirect: 'manual'` on every ragFetch. - W7 #9 F2: added fc00::/7 (IPv6 ULA) to v6 blocklist (AWS IPv6 IMDSv2). - W7 #9 F3: strip trailing `.` before hostname blocklist lookup. - W7 #9 F4: re-validate URL per ragFetch invocation (DNS rebinding + env rotation mitigation). - W7 #9 F9: deleted path.startsWith('http') override branch (future- bypass foot-gun). services/platform/convex/agent_tools/rag/helpers/fetch_document_chunks.ts - W7 #10: pass timeoutMs=60_000 (default 10s was a regression). - Plus MAX_ITERATIONS=30 cap and "cursor did not advance" break to defend against an adversarial RAG response.
Round-2 v09 confirmed: schemas + validators + restoreChatThread all
documented a two-pass grace window (active → expired → cascaded after
graceDays), but the cleanup runner only ever ran a single pass that
cascaded ANY thread whose updatedAt was past the cutoff — including
freshly-trashed threads (no grace window honored) and even active
threads whose user just hadn't messaged in N days. The 'expired'
literal in the validator union was dead — nothing wrote it.
This commit wires the two-pass model the docstrings already promised:
internal_queries.ts
- listExpiredThreads now filters status === 'active' (or unset legacy
rows). Pass-A input.
- New listGraceExpiredThreads filters status IN ('trashed', 'expired')
AND statusChangedAt < graceCutoffMs. Pass-B input.
internal_mutations_retention.ts
- New markThreadExpired internal mutation: idempotent flip from
'active' to 'expired' with statusChangedAt = now. Carries the same
hold-aware + TOCTOU + cross-org guard as the cascade mutations.
Audit subtype: chat_history.retention_expired.
retention_cleanup.cleanupChatHistory
- graceDays === 0 (default): single-pass, cascade active expired
threads directly. Matches pre-Bundle-3 behavior — no trash UX.
- graceDays > 0: Pass A flips active expired threads to 'expired',
giving operators a graceDays-long window to restore via
restoreChatThread. Pass B in the same run cascades trashed/expired
threads whose statusChangedAt is older than the grace cutoff.
Per-thread hold checks fire on BOTH passes; held threads are skipped
with an info log. Existing restoreChatThread already supports both
'trashed' and 'expired' (admin-only for the latter), so the loop is
closed end to end.
Out-of-scope here:
- list_trashed_threads.ts UI wiring (W1 #11 / list_trashed_threads
rewrite — covered separately).
- getThreadStatus carve-out so a bookmarked URL of a trashed thread
renders a restore prompt instead of 404 (W5 #6 — UI follow-up).
Closes #5, #6, #7, #8, #40 — backend billing / ledger correctness. - `checkRuleAgainstUsage` and `collectWarnings` gain a symmetric `prospectiveRequests` parameter. The post-ledger TTS call site (`reserveChunk` → `checkBudget`) passes `1` so an admin who set `maxRequests` for the period sees parallel chunks of a single message honour the cap the same way `maxCostCents` already did. LLM call sites default to 0 — their ledger write is synchronous so retrospective checks stay accurate. - `reserveChunk` overwrite branch: `agentSlug` now falls back to `existing.agentSlug` when the thread temporarily reports no agent on a retry (agent detached between attempts). Without the fallback, ledger writes for the retry landed under the TTS_SLUG sentinel and Top Agents analytics drifted across retries. - `markChunkReadyAndRecordUsage` moves the `!row.userId` check ABOVE the `status: 'ready'` patch. A pending row missing userId now flips to `'failed'` with a `PROVIDER_ERROR` code instead of silently becoming playable while skipping the ledger write. The `synthesize.ts` compensating block handles the resulting throw, deleting the just-uploaded blob and reporting the failure to the client. - `markChunkFailed` now schedules `maybeCleanupChunks` on `index === 0` too, matching the success path. A message whose chunk 0 always fails used to leave the daily cron as the only backstop; the `cleanup:tts` limiter still gates the schedule so a burst of failures can't flood the dispatcher. - `recordTtsUsageInline` adds an in-memory `provider` filter on the upsert lookup. A TTS row with `provider: 'openai'` no longer merges into a sibling LLM row that happens to share (org, user, period, team, agent, model) under a different provider. Latent today on single-TTS-provider configs; load-bearing once a second TTS provider ships. A structural fix (extend the index to include `provider`) is tracked as a follow-up.
Closes #5, #6, #7, #8, #40 — backend billing / ledger correctness. - `checkRuleAgainstUsage` and `collectWarnings` gain a symmetric `prospectiveRequests` parameter. The post-ledger TTS call site (`reserveChunk` → `checkBudget`) passes `1` so an admin who set `maxRequests` for the period sees parallel chunks of a single message honour the cap the same way `maxCostCents` already did. LLM call sites default to 0 — their ledger write is synchronous so retrospective checks stay accurate. - `reserveChunk` overwrite branch: `agentSlug` now falls back to `existing.agentSlug` when the thread temporarily reports no agent on a retry (agent detached between attempts). Without the fallback, ledger writes for the retry landed under the TTS_SLUG sentinel and Top Agents analytics drifted across retries. - `markChunkReadyAndRecordUsage` moves the `!row.userId` check ABOVE the `status: 'ready'` patch. A pending row missing userId now flips to `'failed'` with a `PROVIDER_ERROR` code instead of silently becoming playable while skipping the ledger write. The `synthesize.ts` compensating block handles the resulting throw, deleting the just-uploaded blob and reporting the failure to the client. - `markChunkFailed` now schedules `maybeCleanupChunks` on `index === 0` too, matching the success path. A message whose chunk 0 always fails used to leave the daily cron as the only backstop; the `cleanup:tts` limiter still gates the schedule so a burst of failures can't flood the dispatcher. - `recordTtsUsageInline` adds an in-memory `provider` filter on the upsert lookup. A TTS row with `provider: 'openai'` no longer merges into a sibling LLM row that happens to share (org, user, period, team, agent, model) under a different provider. Latent today on single-TTS-provider configs; load-bearing once a second TTS provider ships. A structural fix (extend the index to include `provider`) is tracked as a follow-up.
Summary by CodeRabbit
Release Notes
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.