Skip to content

switch chat agent to stream output#13

Merged
larryro merged 4 commits into
mainfrom
switch-to-stream
Dec 12, 2025
Merged

switch chat agent to stream output#13
larryro merged 4 commits into
mainfrom
switch-to-stream

Conversation

@larryro

@larryro larryro commented Dec 12, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added image scaling parameter for high-quality rendering (1.0-4.0 range, default 2.0).
    • Real-time message streaming in chat interface with dynamic tool invocation visualization.
  • Improvements

    • Enhanced default image quality (100) and dimensions (1200px width) for better visual output.
    • Improved web search workflow with automatic next-step guidance for fetch operations.
    • Better visibility into active tool usage during agent thinking phase.
  • Documentation

    • Updated guidance on two-step web search process (search then fetch_url).

✏️ Tip: You can customize this high-level summary in your review settings.

- Switch from generateText to streamText with saveStreamDeltas for live updates
- Add getThreadMessagesStreaming query using useUIMessages hook
- Display dynamic tool invocation status in ThinkingAnimation component
  - Show contextual messages like "Searching X" or "Reading example.com"
  - Support multiple concurrent tool calls with smart text grouping
- Improve web_read search tool guidance
  - Add next_action_required field to search results
  - Update agent prompt to emphasize fetch_url after search
  - Add fallback URLs for common queries (weather via wttr.in)
- Add scale parameter for device scale factor (default 2.0 for Retina)
- Increase default quality from 90 to 100 for JPEG images
- Increase default viewport width from 800 to 1200px
- Update image tool description to warn against setting explicit dimensions
- Propagate scale option through crawler service, API endpoints, and Convex types
@coderabbitai

coderabbitai Bot commented Dec 12, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements multi-system image scaling enhancements and streaming-aware chat interface improvements. The image-related changes add a scale parameter (default 2.0, range 1.0–4.0) across the crawler service and document generation, while bumping default quality to 100 and default width to 1200. The chat-related changes introduce streaming message retrieval via useUIMessagesStreaming, refactor ThinkingAnimation to display dynamic tool details extracted from streaming message parts, and add helper utilities for formatting tool information. Additionally, the searchWeb helper now includes a next_action_required field to guide follow-up fetch operations, and generate_agent_response switches from generateText to streamText for real-time streaming responses.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Chat interface refactoring (chat-interface.tsx) involves substantial state management changes, new utility functions, and refactored ThinkingAnimation logic that requires careful tracing of streaming message integration and tool detail extraction patterns.
  • Streaming integration across multiple modules (getThreadMessagesStreaming, getLatestToolMessage, generate_agent_response) introduces new public APIs and delta-accumulation logic that needs verification of correctness and integration points.
  • Image scale propagation across multiple files (converter_service, models, documents, generate_document_helpers, image_tool) requires consistency verification, though most changes are homogeneous parameter additions.
  • Tool workflow changes (searchWeb.next_action_required, web_read_tool documentation) alter the expected agent behavior and need verification against actual tool integration.

Possibly related PRs


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
services/platform/convex/agent_tools/convex_tools/crawler/helpers/search_web.ts (1)

72-90: Escape firstUrl in nextActionRequired (and guard numResults/pageNumber) to avoid malformed instructions/pagination bugs.

firstUrl is interpolated into a JSON-like snippet; if it contains " or control characters, the instruction becomes malformed. Also, numResults <= 0 makes hasMore/next_start_index incorrect.

-    const numResults = args.num_results ?? 10;
-    const pageNumber = args.page_number ?? 1;
+    const numResults = Math.min(50, Math.max(1, args.num_results ?? 10));
+    const pageNumber = Math.max(1, args.page_number ?? 1);
@@
-    const nextActionRequired = hasResults
-      ? `IMPORTANT: These are only search result snippets, NOT the actual page content. To get real data (weather, prices, news, etc.), you MUST now call web_read with { operation: "fetch_url", url: "${firstUrl}" } or another relevant URL from the results above.`
+    const nextActionRequired = hasResults
+      ? `IMPORTANT: These are only search result snippets, NOT the actual page content. To get real data (weather, prices, news, etc.), you MUST now call web_read with { operation: "fetch_url", url: ${JSON.stringify(firstUrl)} } or another relevant URL from the results above.`
       : 'No results found. Try a different search query.';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 92b6cc6 and 75b6d86.

⛔ Files ignored due to path filters (1)
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (17)
  • services/crawler/app/converter_service.py (4 hunks)
  • services/crawler/app/main.py (3 hunks)
  • services/crawler/app/models.py (1 hunks)
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx (8 hunks)
  • services/platform/convex/agent_tools/convex_tools/crawler/helpers/search_web.ts (2 hunks)
  • services/platform/convex/agent_tools/convex_tools/crawler/helpers/types.ts (1 hunks)
  • services/platform/convex/agent_tools/convex_tools/crawler/web_read_tool.ts (1 hunks)
  • services/platform/convex/agent_tools/convex_tools/files/image_tool.ts (2 hunks)
  • services/platform/convex/documents.ts (1 hunks)
  • services/platform/convex/lib/create_chat_agent.ts (1 hunks)
  • services/platform/convex/model/chat_agent/generate_agent_response.ts (1 hunks)
  • services/platform/convex/model/documents/generate_document_helpers.ts (1 hunks)
  • services/platform/convex/model/documents/types.ts (1 hunks)
  • services/platform/convex/model/threads/get_latest_tool_message.ts (1 hunks)
  • services/platform/convex/model/threads/get_thread_messages_streaming.ts (1 hunks)
  • services/platform/convex/model/threads/index.ts (1 hunks)
  • services/platform/convex/threads.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/agent_tools/convex_tools/crawler/helpers/types.ts
  • services/platform/convex/model/documents/generate_document_helpers.ts
  • services/platform/convex/model/threads/index.ts
  • services/platform/convex/model/documents/types.ts
  • services/platform/convex/threads.ts
  • services/platform/convex/model/threads/get_thread_messages_streaming.ts
  • services/platform/convex/model/threads/get_latest_tool_message.ts
  • services/platform/convex/model/chat_agent/generate_agent_response.ts
  • services/crawler/app/models.py
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
  • services/platform/convex/agent_tools/convex_tools/crawler/helpers/search_web.ts
  • services/platform/convex/agent_tools/convex_tools/files/image_tool.ts
  • services/platform/convex/lib/create_chat_agent.ts
  • services/platform/convex/documents.ts
  • services/crawler/app/converter_service.py
  • services/platform/convex/agent_tools/convex_tools/crawler/web_read_tool.ts
  • services/crawler/app/main.py
**/*.{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/agent_tools/convex_tools/crawler/helpers/types.ts
  • services/platform/convex/model/documents/generate_document_helpers.ts
  • services/platform/convex/model/threads/index.ts
  • services/platform/convex/model/documents/types.ts
  • services/platform/convex/threads.ts
  • services/platform/convex/model/threads/get_thread_messages_streaming.ts
  • services/platform/convex/model/threads/get_latest_tool_message.ts
  • services/platform/convex/model/chat_agent/generate_agent_response.ts
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
  • services/platform/convex/agent_tools/convex_tools/crawler/helpers/search_web.ts
  • services/platform/convex/agent_tools/convex_tools/files/image_tool.ts
  • services/platform/convex/lib/create_chat_agent.ts
  • services/platform/convex/documents.ts
  • services/platform/convex/agent_tools/convex_tools/crawler/web_read_tool.ts
**/*.{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/agent_tools/convex_tools/crawler/helpers/types.ts
  • services/platform/convex/model/documents/generate_document_helpers.ts
  • services/platform/convex/model/threads/index.ts
  • services/platform/convex/model/documents/types.ts
  • services/platform/convex/threads.ts
  • services/platform/convex/model/threads/get_thread_messages_streaming.ts
  • services/platform/convex/model/threads/get_latest_tool_message.ts
  • services/platform/convex/model/chat_agent/generate_agent_response.ts
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
  • services/platform/convex/agent_tools/convex_tools/crawler/helpers/search_web.ts
  • services/platform/convex/agent_tools/convex_tools/files/image_tool.ts
  • services/platform/convex/lib/create_chat_agent.ts
  • services/platform/convex/documents.ts
  • services/platform/convex/agent_tools/convex_tools/crawler/web_read_tool.ts
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/threads.ts
  • services/platform/convex/documents.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/figma_rules.mdc)

**/*.{tsx,jsx}: Avoid specifying font-['Inter:Regular',_sans-serif] as it should be the default
Only specify font-family when using non-default fonts like font-['Inter:Medium',_sans-serif]
Ensure font-family matches font-weight (Inter:Regular with font-normal, Inter:Medium with font-medium)
Use leading-normal instead of leading-[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 like text-gray-500, bg-gray-100, border-gray-200; ALWAYS use design system semantic colors: text-foreground for primary text, text-muted-foreground for secondary text and icons, bg-background for main backgrounds, bg-muted for subtle backgrounds and hover states, border-border for borders
ALWAYS use the Table component instead of custom flex layouts; use Table, TableHeader, TableBody, TableRow, TableHead, TableCell components with proper column widths using rem units and semantic colors

Files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
**/app/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)

In Next.js App Router, use page.tsx as server components by default; use 'use client' only for interactions and state

Files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
🧠 Learnings (21)
📚 Learning: 2025-12-02T08:13:51.424Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:51.424Z
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/model/threads/index.ts
  • services/platform/convex/threads.ts
  • services/platform/convex/model/threads/get_thread_messages_streaming.ts
  • services/platform/convex/model/threads/get_latest_tool_message.ts
📚 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 services/**/convex/*.ts : Thin wrapper API modules in services may export multiple Convex functions as thin wrappers that delegate to model helpers, must use snake_case file names and camelCase export names, and must not contain business logic

Applied to files:

  • services/platform/convex/model/threads/index.ts
  • services/platform/convex/threads.ts
  • services/platform/convex/model/threads/get_latest_tool_message.ts
📚 Learning: 2025-12-02T08:13:24.290Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.290Z
Learning: Applies to convex/**/*.{ts,tsx} : When touching existing modules that export multiple functions, split them into separate files so each file exports exactly one function matching the file name, unless the module is an explicitly designated thin wrapper file.

Applied to files:

  • services/platform/convex/model/threads/index.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/**/!(model)/*.ts : Exception: Thin wrapper API modules (e.g., `services/platform/convex/documents.ts`) may export multiple Convex functions if they only validate args/returns and delegate to the model layer; must use snake_case for file names and camelCase for function names

Applied to files:

  • services/platform/convex/model/threads/index.ts
  • services/platform/convex/model/threads/get_latest_tool_message.ts
📚 Learning: 2025-12-02T08:13:24.290Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.290Z
Learning: Applies to convex/**/convex/*.{ts,tsx} : Thin wrapper API modules (e.g., `services/platform/convex/documents.ts`) may export multiple Convex functions as thin wrappers around model-layer helpers. These files must not contain business logic, should only validate args/returns and delegate to the model layer, must use snake_case for file names and camelCase for each exported function.

Applied to files:

  • services/platform/convex/model/threads/index.ts
📚 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 convex/*.ts : Split existing Convex modules that export multiple functions into separate files with one function per file, except for explicitly designated thin wrapper modules

Applied to files:

  • services/platform/convex/model/threads/index.ts
📚 Learning: 2025-12-02T08:13:51.424Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:51.424Z
Learning: Applies to convex/**/*.ts : Split existing Convex modules that export multiple functions into separate files, each exporting exactly one function, except for thin wrapper modules

Applied to files:

  • services/platform/convex/model/threads/index.ts
📚 Learning: 2025-12-02T08:13:24.290Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.290Z
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/threads.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/threads.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 : Define pagination using `paginationOptsValidator` with `numItems` and `cursor` properties, and use `.paginate()` on queries which returns objects with `page`, `isDone`, and `continueCursor` properties

Applied to files:

  • services/platform/convex/threads.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 `api` object from `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`

Applied to files:

  • services/platform/convex/threads.ts
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
📚 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/threads.ts
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
📚 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/threads.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/threads.ts
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx
📚 Learning: 2025-12-02T08:13:24.290Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:24.290Z
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/convex/threads.ts
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.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} : Register internal functions with internalQuery, internalMutation, and internalAction (imported from ./_generated/server)

Applied to files:

  • services/platform/convex/threads.ts
  • services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.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} : Minimize calls from actions to queries/mutations to avoid race conditions

Applied to files:

  • services/platform/convex/threads.ts
📚 Learning: 2025-07-20T08:40:24.693Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/ai.mdc:0-0
Timestamp: 2025-07-20T08:40:24.693Z
Learning: Applies to **/app/api/**/*.ts : For text generation, use the `generateObject` or similar functions from the AI SDK

Applied to files:

  • services/platform/convex/model/chat_agent/generate_agent_response.ts
  • services/platform/convex/lib/create_chat_agent.ts
📚 Learning: 2025-07-20T08:40:24.693Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/ai.mdc:0-0
Timestamp: 2025-07-20T08:40:24.693Z
Learning: Applies to **/actions/*.ts : For text generation, use the `generateObject` or similar functions from the AI SDK

Applied to files:

  • services/platform/convex/model/chat_agent/generate_agent_response.ts
  • services/platform/convex/lib/create_chat_agent.ts
📚 Learning: 2025-07-20T08:40:24.693Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/ai.mdc:0-0
Timestamp: 2025-07-20T08:40:24.693Z
Learning: Applies to **/lib/**/*.ts : For text generation, use the `generateObject` or similar functions from the AI SDK

Applied to files:

  • services/platform/convex/model/chat_agent/generate_agent_response.ts
  • services/platform/convex/lib/create_chat_agent.ts
📚 Learning: 2025-08-21T14:59:56.034Z
Learnt from: CR
Repo: talecorp/lanserhof PR: 0
File: .cursor/rules/ai.mdc:0-0
Timestamp: 2025-08-21T14:59:56.034Z
Learning: Applies to {**/actions/*.ts,**/app/api/**/*.ts,**/lib/**/*.ts} : For text generation, use `generateObject` (or equivalent SDK functions) with an SDK-provided model (e.g., `openrouter(...)`)

Applied to files:

  • services/platform/convex/model/chat_agent/generate_agent_response.ts
🧬 Code graph analysis (3)
services/platform/convex/threads.ts (2)
services/platform/convex/model/threads/get_latest_tool_message.ts (1)
  • getLatestToolMessage (74-123)
services/platform/convex/model/threads/get_thread_messages_streaming.ts (1)
  • getThreadMessagesStreaming (25-51)
services/platform/convex/model/threads/get_thread_messages_streaming.ts (2)
services/platform/convex/model/threads/index.ts (2)
  • StreamingMessagesResult (18-18)
  • getThreadMessagesStreaming (13-13)
services/platform/convex/threads.ts (1)
  • getThreadMessagesStreaming (187-200)
services/platform/convex/model/threads/get_latest_tool_message.ts (2)
services/platform/convex/model/threads/index.ts (2)
  • LatestToolMessage (17-17)
  • getLatestToolMessage (12-12)
services/platform/convex/threads.ts (1)
  • getLatestToolMessage (168-180)
🪛 Ruff (0.14.8)
services/crawler/app/models.py

119-119: Boolean positional value in function call

(FBT003)

services/crawler/app/converter_service.py

237-237: Boolean-typed positional argument in function definition

(FBT001)


237-237: Boolean default positional argument in function definition

(FBT002)


334-334: Boolean-typed positional argument in function definition

(FBT001)


334-334: Boolean default positional argument in function definition

(FBT002)

⏰ 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 Docker Image
🔇 Additional comments (19)
services/platform/convex/model/chat_agent/generate_agent_response.ts (2)

303-327: LGTM! Streaming setup is correct.

The switch to streamText with saveStreamDeltas: true properly enables real-time streaming for UI updates. The parameter structure and context options are correct for the streaming API.


329-341: LGTM! Stream consumption pattern is correct.

The implementation correctly:

  1. Iterates through textStream to accumulate the final text
  2. Awaits steps and usage promises after stream completion
  3. Assembles the result with proper typing

This follows the expected pattern for consuming streaming responses from the AI SDK.

services/platform/convex/agent_tools/convex_tools/crawler/web_read_tool.ts (1)

94-117: The documentation lists an inaccurate set of search engines—DuckDuckGo is actually disabled in the SearXNG configuration, and Wikipedia and Startpage are enabled but not mentioned.

Update the description at lines 101-102 to reflect the actual enabled engines: Brave, Google, Bing, Wikipedia, and Startpage (DuckDuckGo is disabled in the default configuration).

Alternatively, make the description config-agnostic: "Aggregates results from multiple search engines (depending on configuration)."

services/platform/convex/agent_tools/convex_tools/crawler/helpers/types.ts (1)

61-65: No breaking change—next_action_required is always assigned by the sole producer searchWeb().

The field was already being set in all code paths (lines 75–89 of search_web.ts), so making it required in the type signature simply documents existing behavior. All instantiations already provide it, and no other code constructs WebReadSearchResult objects.

The refactoring suggestion to use a structured field (e.g., { type: 'fetch_url'|'none', url?: string, reason?: string }) is worth considering for maintainability and robustness, but it is optional—the current string-based approach works correctly since it's generated internally by searchWeb(), not parsed from external input.

services/crawler/app/models.py (1)

118-121: Acknowledge that improved image quality is an intentional feature enhancement, not a breaking change.

The changes to ImageOptions defaults (quality 90→100, width 800→1200, scale 2.0 for Retina support) are documented in commit 75b6d86 as part of a feature to improve image generation quality with high-DPI support. This is an intentional enhancement, not an undocumented breaking change.

The implementation is sound:

  • Code comments in converter_service.py explicitly document the purpose ("high-quality rendering" and "high-quality output")
  • Descriptions are clear in English per guidelines
  • UrlToImageRequest already overrides width to 1280, showing deliberate consideration of width adjustments
  • The scale parameter properly defaults to "device" mode for Retina displays when scale > 1.0

No action required—this is a documented feature enhancement.

services/platform/convex/lib/create_chat_agent.ts (1)

112-127: Clear documentation of the two-step web_read pattern.

The expanded guidance effectively clarifies that the search operation returns only URLs with snippets and that a follow-up fetch_url call is required to retrieve actual content. The step-by-step usage pattern is well-structured and will help the agent correctly retrieve real-world data.

services/platform/convex/threads.ts (1)

163-180: LGTM!

The query is well-structured with proper validation and follows the thin wrapper pattern by delegating to the model layer.

services/platform/convex/model/threads/index.ts (1)

12-18: LGTM!

The new exports are properly structured and follow the module's re-export pattern.

services/platform/convex/model/threads/get_latest_tool_message.ts (3)

1-23: LGTM!

The interface is well-designed with clear documentation explaining its purpose for displaying dynamic loading status in the UI.


24-72: LGTM!

The helper functions are well-implemented with appropriate type handling for dynamic message content. The use of Set for deduplication in extractToolNames is a good practice.


74-123: LGTM!

The function logic correctly identifies the latest tool-related message and determines its status. The approach of fetching 10 recent messages and checking for both tool role messages and assistant messages with tool-calls is comprehensive and well-structured.

services/platform/app/(app)/dashboard/[id]/chat/components/chat-interface.tsx (4)

42-68: LGTM!

The utility functions are well-implemented with appropriate error handling. The extractHostname function correctly handles invalid URLs, and truncate uses the proper ellipsis character.


70-132: LGTM!

The formatToolDetail function provides comprehensive tool formatting with context-aware display text. The special handling for web_read operations and rag_search with query details enhances the user experience during tool execution.


523-528: LGTM!

The conditional rendering logic correctly displays the ThinkingAnimation only when loading and before streaming text appears, providing a smooth user experience during the transition from tool execution to text streaming.


263-283: UIMessage.key is a valid unique identifier for message IDs.

The key property in UIMessage (from @convex-dev/agent/react) is a string that serves as a unique identifier, making it appropriate for use as the id when converting to ChatMessage format on line 274.

services/platform/convex/agent_tools/convex_tools/files/image_tool.ts (1)

44-44: LGTM! Scale parameter correctly integrated.

The addition of the scale parameter with proper validation (1.0–4.0 range) and the updated guidance about auto-sizing dimensions align well with high-quality image rendering requirements. The default of 2.0 is appropriate for Retina displays.

Also applies to: 78-81

services/platform/convex/model/documents/types.ts (1)

133-133: LGTM! Type definition is well-documented.

The scale parameter is properly typed with a clear comment documenting the valid range (1.0–4.0) and default value (2.0).

services/crawler/app/main.py (1)

410-410: Scale validation is already implemented in Pydantic models.

The scale parameter has proper validation in place. Both ImageOptions and the request models include scale validation with constraints ge=1.0, le=4.0, ensuring only values in the 1.0–4.0 range are accepted. Pydantic will automatically reject invalid values during request validation, so no additional changes are needed.

services/platform/convex/model/documents/generate_document_helpers.ts (1)

113-116: These default changes were intentional and documented as part of image quality improvements.

The commit "feat: improve image generation quality with high-DPI support" explicitly documents increasing quality from 90 to 100, width from 800 to 1200px, and adding scale 2.0 for Retina displays. While the combined effect increases file sizes (potentially 5–10×), this tradeoff was intentional for high-DPI support.

Configuration options already exist—imageOptions accepts optional parameters to override quality, width, and scale on a per-call basis. If infrastructure capacity is a concern, document the available customization options (e.g., lower quality or scale for cost-sensitive scenarios).

Comment thread services/crawler/app/converter_service.py
Comment thread services/crawler/app/converter_service.py
Comment thread services/crawler/app/converter_service.py
Comment thread services/platform/convex/documents.ts
Comment thread services/platform/convex/lib/create_chat_agent.ts
Comment thread services/platform/convex/threads.ts
…agnostics

- Delete context_overflow_retry.ts helper module
- Instead of retrying on empty responses, throw detailed error with:
  - Model info, token counts, step-by-step execution details
  - Finish reasons, tool call counts, warnings, and provider errors
- Add finishReason and warnings to stream result type
- Simplify generate_agent_response by removing retry logic
@larryro larryro merged commit 04038a8 into main Dec 12, 2025
2 checks passed
@larryro larryro deleted the switch-to-stream branch December 12, 2025 14:31
yannickmonney pushed a commit that referenced this pull request Apr 8, 2026
larryro added a commit that referenced this pull request May 7, 2026
…OCTOU

Adds assertSafeRetentionDelete to internal_mutations_retention.ts and
threads the new cutoffMs through every retention dispatcher call site.
The guard runs in-mutation (V8 transaction) and closes three round-2
findings simultaneously:

- W1 #4 — snapshot-race vs legal hold. The dispatcher's
  loadActiveHoldsForOrg snapshot is up to 25 min stale; a hold placed
  AFTER snapshot but BEFORE the per-row mutation otherwise had zero
  protection. Re-reading inside each mutation makes the in-mutation
  read the only authoritative gate.
- W6 #10 — cross-org corruption. Every deleteExpired* trusted
  args.organizationId blindly; a swapped id silently deleted org A's
  row while logging the audit under org B (and forking the per-org
  hash chain). Now we assert row.organizationId === args.organizationId.
- W4 #13 — cutoff TOCTOU. Between the dispatcher's listExpired query
  and the per-row delete, a user can re-touch the row (chat
  thread.updatedAt bump, document patch). The mutation now re-evaluates
  cutoff against (updatedAt ?? _creationTime) and skips when the row
  is no longer eligible.

Mutations updated (each accepts new optional cutoffMs):
- deleteExpiredDocument            (targetType: 'document')
- deleteExpiredThread              (targetType: 'thread')
- deleteExpiredWorkflowExecution   (targetType: 'execution')
- deleteExpiredWorkflowTriggerLog  (orgId + cutoff only)
- deleteExpiredCustomer / Vendor / ExternalConversation /
  PromptTemplate / MessageFeedback / MemoryAuditRow /
  ChatFilterEvent / UsageLedgerRow (orgId + cutoff)

Out-of-scope here:
- deleteExpiredTempFile, deleteExpiredMessageMetadata,
  deleteExpiredTwoFactorAttempt, deleteExpiredLoginAttempt,
  deleteExpiredLoginBlockCounter — none of these carry organizationId
  on their args today (they're either cross-org-by-shape or have an
  indirect link). The orgHeld / cross-org checks are not applicable
  without an org backreference, which is itself a separate phase-10
  follow-up. Their existing absence-of-row idempotence is preserved.
larryro added a commit that referenced this pull request May 7, 2026
…cies

Bundle of round-2-confirmed cross-tenant fixes plus the dead-code
delete of the semantic LLM response cache.

POLICY_TYPES drift (W6 #5)
- lib/shared/schemas/governance.ts now includes
  'data_classification_notice' to match the Convex enum, killing the
  `as const` cast at use-data-classification-notice.ts:50.

documents/compare_documents.ts (W6 #8)
- Convex `_storage` is a global namespace; org membership alone was
  not enough to gate `ctx.storage.getUrl`. Adds a JOIN through
  fileMetadata via the new internal query verifyStorageIdsBelongToOrg
  to confirm both `baseStorageId` and `comparisonStorageId` are owned
  by the caller's org. Refuses with a clear error otherwise. Pattern
  copied from agent_tools/documents/helpers/retrieve_document.ts.

file_metadata/actions.ts::checkFileRagStatuses (W6 #9)
- Was an unauthenticated public action that could flip any org's
  fileMetadata.ragStatus to `failed` via expireStaleRagQueue (DoS,
  pre-existing on `main`). Now requires `getAuthUser` and filters
  storageIds to ones owned by an org the caller is a member of via
  the new file_metadata.internal_queries.filterStorageIdsByCallerOrg.

governance/queries.ts (W6 #11)
- getPolicy + listPolicies now apply a member-readable allow-list
  (data_classification_notice, feature_flags, pii_config,
  chat_filter, personalization, upload_policy, default_models). All
  other types — login_policy.trustedProxies, password_policy,
  two_factor_policy, model_access.rules, budgets, retention_policy,
  moderation_provider.endpoint, system_prompt — are admin-only.
  listPolicies silently filters those out for non-admins.

semantic LLM response cache — DELETE (W6 #12 + #13)
- Round-2 v05 confirmed the lookup is structurally cross-tenant
  (filters only on agent_name, model, expires_at, similarity; ignores
  user_id / organization_id even though they're stored). The platform
  helpers `lookupSemanticCache` / `storeSemanticCacheAsync` had ZERO
  callers in the monorepo, the FastAPI router was mounted but
  unreachable from platform — a latent foot-gun primed for the next
  dev to wire up unaware. Deletes:

  - services/platform/convex/lib/response_cache/semantic_cache.ts
  - services/platform/convex/lib/response_cache/internal_actions.ts
  - services/rag/app/routers/llm_cache.py
  - services/rag/app/services/llm_response_cache.py

  Plus the corresponding imports in routers/__init__.py, main.py,
  rag_service.py. Also removes the two empty-catch violations in
  semantic_cache.ts (no longer applicable).

The exact-key Convex `lib/response_cache/{internal_mutations,
internal_queries}.ts` cache stays — it is the actually-wired one and
is correctly org-scoped.
larryro added a commit that referenced this pull request May 8, 2026
…OCTOU

Adds assertSafeRetentionDelete to internal_mutations_retention.ts and
threads the new cutoffMs through every retention dispatcher call site.
The guard runs in-mutation (V8 transaction) and closes three round-2
findings simultaneously:

- W1 #4 — snapshot-race vs legal hold. The dispatcher's
  loadActiveHoldsForOrg snapshot is up to 25 min stale; a hold placed
  AFTER snapshot but BEFORE the per-row mutation otherwise had zero
  protection. Re-reading inside each mutation makes the in-mutation
  read the only authoritative gate.
- W6 #10 — cross-org corruption. Every deleteExpired* trusted
  args.organizationId blindly; a swapped id silently deleted org A's
  row while logging the audit under org B (and forking the per-org
  hash chain). Now we assert row.organizationId === args.organizationId.
- W4 #13 — cutoff TOCTOU. Between the dispatcher's listExpired query
  and the per-row delete, a user can re-touch the row (chat
  thread.updatedAt bump, document patch). The mutation now re-evaluates
  cutoff against (updatedAt ?? _creationTime) and skips when the row
  is no longer eligible.

Mutations updated (each accepts new optional cutoffMs):
- deleteExpiredDocument            (targetType: 'document')
- deleteExpiredThread              (targetType: 'thread')
- deleteExpiredWorkflowExecution   (targetType: 'execution')
- deleteExpiredWorkflowTriggerLog  (orgId + cutoff only)
- deleteExpiredCustomer / Vendor / ExternalConversation /
  PromptTemplate / MessageFeedback / MemoryAuditRow /
  ChatFilterEvent / UsageLedgerRow (orgId + cutoff)

Out-of-scope here:
- deleteExpiredTempFile, deleteExpiredMessageMetadata,
  deleteExpiredTwoFactorAttempt, deleteExpiredLoginAttempt,
  deleteExpiredLoginBlockCounter — none of these carry organizationId
  on their args today (they're either cross-org-by-shape or have an
  indirect link). The orgHeld / cross-org checks are not applicable
  without an org backreference, which is itself a separate phase-10
  follow-up. Their existing absence-of-row idempotence is preserved.
larryro added a commit that referenced this pull request May 8, 2026
…cies

Bundle of round-2-confirmed cross-tenant fixes plus the dead-code
delete of the semantic LLM response cache.

POLICY_TYPES drift (W6 #5)
- lib/shared/schemas/governance.ts now includes
  'data_classification_notice' to match the Convex enum, killing the
  `as const` cast at use-data-classification-notice.ts:50.

documents/compare_documents.ts (W6 #8)
- Convex `_storage` is a global namespace; org membership alone was
  not enough to gate `ctx.storage.getUrl`. Adds a JOIN through
  fileMetadata via the new internal query verifyStorageIdsBelongToOrg
  to confirm both `baseStorageId` and `comparisonStorageId` are owned
  by the caller's org. Refuses with a clear error otherwise. Pattern
  copied from agent_tools/documents/helpers/retrieve_document.ts.

file_metadata/actions.ts::checkFileRagStatuses (W6 #9)
- Was an unauthenticated public action that could flip any org's
  fileMetadata.ragStatus to `failed` via expireStaleRagQueue (DoS,
  pre-existing on `main`). Now requires `getAuthUser` and filters
  storageIds to ones owned by an org the caller is a member of via
  the new file_metadata.internal_queries.filterStorageIdsByCallerOrg.

governance/queries.ts (W6 #11)
- getPolicy + listPolicies now apply a member-readable allow-list
  (data_classification_notice, feature_flags, pii_config,
  chat_filter, personalization, upload_policy, default_models). All
  other types — login_policy.trustedProxies, password_policy,
  two_factor_policy, model_access.rules, budgets, retention_policy,
  moderation_provider.endpoint, system_prompt — are admin-only.
  listPolicies silently filters those out for non-admins.

semantic LLM response cache — DELETE (W6 #12 + #13)
- Round-2 v05 confirmed the lookup is structurally cross-tenant
  (filters only on agent_name, model, expires_at, similarity; ignores
  user_id / organization_id even though they're stored). The platform
  helpers `lookupSemanticCache` / `storeSemanticCacheAsync` had ZERO
  callers in the monorepo, the FastAPI router was mounted but
  unreachable from platform — a latent foot-gun primed for the next
  dev to wire up unaware. Deletes:

  - services/platform/convex/lib/response_cache/semantic_cache.ts
  - services/platform/convex/lib/response_cache/internal_actions.ts
  - services/rag/app/routers/llm_cache.py
  - services/rag/app/services/llm_response_cache.py

  Plus the corresponding imports in routers/__init__.py, main.py,
  rag_service.py. Also removes the two empty-catch violations in
  semantic_cache.ts (no longer applicable).

The exact-key Convex `lib/response_cache/{internal_mutations,
internal_queries}.ts` cache stays — it is the actually-wired one and
is correctly org-scoped.
larryro added a commit that referenced this pull request May 8, 2026
…OCTOU

Adds assertSafeRetentionDelete to internal_mutations_retention.ts and
threads the new cutoffMs through every retention dispatcher call site.
The guard runs in-mutation (V8 transaction) and closes three round-2
findings simultaneously:

- W1 #4 — snapshot-race vs legal hold. The dispatcher's
  loadActiveHoldsForOrg snapshot is up to 25 min stale; a hold placed
  AFTER snapshot but BEFORE the per-row mutation otherwise had zero
  protection. Re-reading inside each mutation makes the in-mutation
  read the only authoritative gate.
- W6 #10 — cross-org corruption. Every deleteExpired* trusted
  args.organizationId blindly; a swapped id silently deleted org A's
  row while logging the audit under org B (and forking the per-org
  hash chain). Now we assert row.organizationId === args.organizationId.
- W4 #13 — cutoff TOCTOU. Between the dispatcher's listExpired query
  and the per-row delete, a user can re-touch the row (chat
  thread.updatedAt bump, document patch). The mutation now re-evaluates
  cutoff against (updatedAt ?? _creationTime) and skips when the row
  is no longer eligible.

Mutations updated (each accepts new optional cutoffMs):
- deleteExpiredDocument            (targetType: 'document')
- deleteExpiredThread              (targetType: 'thread')
- deleteExpiredWorkflowExecution   (targetType: 'execution')
- deleteExpiredWorkflowTriggerLog  (orgId + cutoff only)
- deleteExpiredCustomer / Vendor / ExternalConversation /
  PromptTemplate / MessageFeedback / MemoryAuditRow /
  ChatFilterEvent / UsageLedgerRow (orgId + cutoff)

Out-of-scope here:
- deleteExpiredTempFile, deleteExpiredMessageMetadata,
  deleteExpiredTwoFactorAttempt, deleteExpiredLoginAttempt,
  deleteExpiredLoginBlockCounter — none of these carry organizationId
  on their args today (they're either cross-org-by-shape or have an
  indirect link). The orgHeld / cross-org checks are not applicable
  without an org backreference, which is itself a separate phase-10
  follow-up. Their existing absence-of-row idempotence is preserved.
larryro added a commit that referenced this pull request May 8, 2026
…cies

Bundle of round-2-confirmed cross-tenant fixes plus the dead-code
delete of the semantic LLM response cache.

POLICY_TYPES drift (W6 #5)
- lib/shared/schemas/governance.ts now includes
  'data_classification_notice' to match the Convex enum, killing the
  `as const` cast at use-data-classification-notice.ts:50.

documents/compare_documents.ts (W6 #8)
- Convex `_storage` is a global namespace; org membership alone was
  not enough to gate `ctx.storage.getUrl`. Adds a JOIN through
  fileMetadata via the new internal query verifyStorageIdsBelongToOrg
  to confirm both `baseStorageId` and `comparisonStorageId` are owned
  by the caller's org. Refuses with a clear error otherwise. Pattern
  copied from agent_tools/documents/helpers/retrieve_document.ts.

file_metadata/actions.ts::checkFileRagStatuses (W6 #9)
- Was an unauthenticated public action that could flip any org's
  fileMetadata.ragStatus to `failed` via expireStaleRagQueue (DoS,
  pre-existing on `main`). Now requires `getAuthUser` and filters
  storageIds to ones owned by an org the caller is a member of via
  the new file_metadata.internal_queries.filterStorageIdsByCallerOrg.

governance/queries.ts (W6 #11)
- getPolicy + listPolicies now apply a member-readable allow-list
  (data_classification_notice, feature_flags, pii_config,
  chat_filter, personalization, upload_policy, default_models). All
  other types — login_policy.trustedProxies, password_policy,
  two_factor_policy, model_access.rules, budgets, retention_policy,
  moderation_provider.endpoint, system_prompt — are admin-only.
  listPolicies silently filters those out for non-admins.

semantic LLM response cache — DELETE (W6 #12 + #13)
- Round-2 v05 confirmed the lookup is structurally cross-tenant
  (filters only on agent_name, model, expires_at, similarity; ignores
  user_id / organization_id even though they're stored). The platform
  helpers `lookupSemanticCache` / `storeSemanticCacheAsync` had ZERO
  callers in the monorepo, the FastAPI router was mounted but
  unreachable from platform — a latent foot-gun primed for the next
  dev to wire up unaware. Deletes:

  - services/platform/convex/lib/response_cache/semantic_cache.ts
  - services/platform/convex/lib/response_cache/internal_actions.ts
  - services/rag/app/routers/llm_cache.py
  - services/rag/app/services/llm_response_cache.py

  Plus the corresponding imports in routers/__init__.py, main.py,
  rag_service.py. Also removes the two empty-catch violations in
  semantic_cache.ts (no longer applicable).

The exact-key Convex `lib/response_cache/{internal_mutations,
internal_queries}.ts` cache stays — it is the actually-wired one and
is correctly org-scoped.
larryro added a commit that referenced this pull request May 9, 2026
…stodian cascade

Round-1 #10/#12/#13 + round-2 V3 confirmed three custodian-cascade gaps
(P0-6 retention guard, P0-7 deleteDocumentById, P0-8 cleanupMessageMetadata
+ cleanupChatFilterEvents). The user's observation made the deeper truth
clear: in production, only `org` and `userMembership` hold target types
are operator-facing — `thread` / `document` / `execution` were modeled in
the original fine-grained hold design but never wired into the place-hold
UI (`PICKER_TARGET_TYPES = ['userMembership', 'org']`), and `usePlaceLegalHold`
is called from a single dialog. The corresponding `holds.threadIds` /
`documentIds` / `executionIds` Sets in `ActiveHolds` therefore stayed
empty in every deployment, while ~80 LOC of dead per-row checks
proliferated through retention, erasure, restore, folders, and the
cascade helper. This commit collapses the model to its actual shape.

Schema-level narrowing (legal_hold.ts):
  - HOLD_TARGET_TYPES = ['userMembership', 'org'] (write-side)
  - ActiveHolds = { orgHeld, userMembershipIds }
  - loadActiveHolds switch reduces to 2 cases; legacy thread/document/
    execution rows in the table (if any) are intentionally ignored —
    they're surfaced in admin UI as `(legacy)` via the read-side
    validator in legal_hold_queries.ts.
  - resolveAndAssertTarget keeps only `org` + `userMembership` cases.
  - isHeld() simplified to org + custodian.

Guard simplification (legal_hold_guard.ts):
  - assertNotHeld drops the per-row branches; `targetType`/`targetId`
    are now error-message context only. The user-custodian cascade
    via `authorUserId` is the sole remaining gate beyond org-wide.

assertSafeRetentionDelete (internal_mutations_retention.ts):
  - Drops `targetType?` / `targetId?` args; adds `authorUserId?`.
  - Internal handler is now: cross-org check → TOCTOU check →
    org-wide hold → user-custodian cascade. Single, clear contract.

P0-6 cascade wiring — every retention mutation passes the row's
author user id (~15 call sites in internal_mutations_retention.ts):
  - documents → doc.createdBy
  - threads → thread.userId
  - workflowExecution → execution.userId
  - workflowTriggerLog → exec.userId via wfExecutionId join
  - messageFeedback → row.userId
  - memoryAudit → row.subjectUserId (with thread-fallback)
  - chatFilterEvent → parent thread's userId via index lookup
  - usageLedger → row.userId
  - promptTemplate → row.createdBy
  - messageMetadata → parent thread's userId (closes P0-8 directly)
  - tempFile, customer, vendor, externalConversation: org-only
    (no author concept on the row)

P0-8 cascade fix — cleanupMessageMetadata + cleanupChatFilterEvents
no longer rely on the now-empty `holds.threadIds` Set; both look up
the parent thread's `userId` for the user-membership cascade
(messageMetadata cleanup mutation was already P0-8 fixed in commit 1).

Other dead-check removals:
  - retention_cleanup.ts: ~10 dead `holds.threadIds.has` /
    `documentIds.has` / `executionIds.has` checks removed; per-row
    custodian cascade either replaces them or the mutation re-checks.
  - governance/restore.ts: drops dead per-row branches; user-custodian
    cascade in restoreSoftDeletedRow (commit 1) covers the rest.
  - threads/cascade_helpers.ts: per-thread hold check replaced with
    a thread-metadata lookup → `userMembershipIds.has(thread.userId)`.
  - threads/delete_chat_thread.ts + restore_chat_thread.ts: drop
    `holds.threadIds.has(threadId)`; ownerHeld + orgHeld cover all
    refusal cases.
  - folders/mutations.ts assertNoHeldDescendantDocs: per-document
    hold check replaced with `userMembershipIds.has(doc.createdBy)`.
  - governance/erasure.ts requestErasure: drops heldThreadIds /
    heldDocumentIds collection — refusal is now `orgHeld ||
    userCustodianHeld`. eraseSubjectDocuments drops the per-row
    held-document check (subject is by definition the only author
    in the iteration; user-custodian gate applied at request time).
  - legal_hold_internal.ts: loadActiveHoldsForOrg returns the
    narrowed ActiveHolds shape across the action/query boundary.

Tests rewritten — `governance/__tests__/retention_legal_hold_gaps.test.ts`:
  - Drops thread/document/execution per-row hold cases (behaviour
    deleted on purpose).
  - Adds 6 cases covering the simplified contract: authorUserId
    cascade, TOCTOU + cross-org pre-checks, missing authorUserId
    grandfather, org-wide hold blanket refusal.
  - Source-grep regressions: every retention mutation passes the
    right authorUserId; cleanup actions pre-filter via the right
    `holds.userMembershipIds.has(...)`; per-row Sets stay deleted
    from the cleanup / erasure / guard sources.

Note: this is a one-way migration. Any deployment with active
legacy thread/document/execution holds loses enforcement of those
holds going forward; the read-side validator in legal_hold_queries
keeps showing them as `(legacy)`. Acceptable per project memory
(demo stage, no prod use). Pre-prod check: convert to user/org
holds first if any deployment has them.

Verified: typecheck clean; 851 tests pass across affected dirs;
full vitest run shows 5641/5642 pass (1 pre-existing flake in
canvas-pane.test.tsx, unrelated).
larryro added a commit that referenced this pull request May 9, 2026
…te gates

P0-9 — Org-wide hold UI invisibility (round-1 #12, round-2 V4):
  Before: `heldByTargetValidator` declared `via: 'org'` but no producer
  ever emitted it. With only an org-wide hold active, `getLegalHoldByTarget`
  returned null and `listActiveHoldTargetIds` returned an empty list →
  no chat-sidebar lock icons, no admin-grid badges, no banner. Backend
  refused destructive actions but the UX showed nothing until the user
  attempted a delete and hit a confusing error.
  Fix:
  - `getLegalHoldByTarget`: third pass after direct + custodian-cascade
    looks up the org-wide hold row and surfaces it with `via: 'org'`.
  - `listActiveHoldTargetIds`: returns a new `orgHeld: boolean` flag
    alongside the per-target id set; the chat-sidebar ORs the flag with
    the per-thread set so every row gets the lock indicator when the
    whole org is on a hold.
  - `chat-history-sidebar.tsx`: replaces direct `heldThreadIds.has(id)`
    checks with an `isThreadHeld(id)` helper that ORs `orgWideHeld`.

P0-10 — `prepareOrganizationDeletion` bypasses holds (round-1 #13, V4):
  `cascadeOnOrgDeleted` hard-deletes userMemories + userPreferences (and
  per Commit 1, every thread-bound chat-upload file) for every member of
  the org. The mutation refused only on owner-role check; legal-hold
  status was never consulted. Adds `assertNotHeld(ctx, orgId, 'org',
  orgId, undefined, authUser._id)` before the cascade — refuses on org-wide
  hold OR on a userMembership cascade against the owner deleting.
  `GuardedTargetType` extended with `'org'` for the error-payload context.

P0-11 — `removeMember` bypasses custodian holds (round-1 #13, V4):
  `cascadeOnMemberRemoved` cascades into userMemories + userPreferences
  (+ chat-upload files) scoped to the member being removed. Mutation
  previously checked only role. Adds `assertNotHeld(ctx, orgId,
  'userMembership', member.userId, undefined, member.userId)` before
  the cascade — refuses when the org is on a hold OR when the member
  themselves is on a custodian hold. Self-author cascade uses the same
  user id for `targetId` and `authorUserId` so the gate fires regardless
  of which leg matches.

Verified: typecheck clean; 908 tests pass across affected dirs.
larryro added a commit that referenced this pull request May 9, 2026
…OCTOU

Adds assertSafeRetentionDelete to internal_mutations_retention.ts and
threads the new cutoffMs through every retention dispatcher call site.
The guard runs in-mutation (V8 transaction) and closes three round-2
findings simultaneously:

- W1 #4 — snapshot-race vs legal hold. The dispatcher's
  loadActiveHoldsForOrg snapshot is up to 25 min stale; a hold placed
  AFTER snapshot but BEFORE the per-row mutation otherwise had zero
  protection. Re-reading inside each mutation makes the in-mutation
  read the only authoritative gate.
- W6 #10 — cross-org corruption. Every deleteExpired* trusted
  args.organizationId blindly; a swapped id silently deleted org A's
  row while logging the audit under org B (and forking the per-org
  hash chain). Now we assert row.organizationId === args.organizationId.
- W4 #13 — cutoff TOCTOU. Between the dispatcher's listExpired query
  and the per-row delete, a user can re-touch the row (chat
  thread.updatedAt bump, document patch). The mutation now re-evaluates
  cutoff against (updatedAt ?? _creationTime) and skips when the row
  is no longer eligible.

Mutations updated (each accepts new optional cutoffMs):
- deleteExpiredDocument            (targetType: 'document')
- deleteExpiredThread              (targetType: 'thread')
- deleteExpiredWorkflowExecution   (targetType: 'execution')
- deleteExpiredWorkflowTriggerLog  (orgId + cutoff only)
- deleteExpiredCustomer / Vendor / ExternalConversation /
  PromptTemplate / MessageFeedback / MemoryAuditRow /
  ChatFilterEvent / UsageLedgerRow (orgId + cutoff)

Out-of-scope here:
- deleteExpiredTempFile, deleteExpiredMessageMetadata,
  deleteExpiredTwoFactorAttempt, deleteExpiredLoginAttempt,
  deleteExpiredLoginBlockCounter — none of these carry organizationId
  on their args today (they're either cross-org-by-shape or have an
  indirect link). The orgHeld / cross-org checks are not applicable
  without an org backreference, which is itself a separate phase-10
  follow-up. Their existing absence-of-row idempotence is preserved.
larryro added a commit that referenced this pull request May 9, 2026
…cies

Bundle of round-2-confirmed cross-tenant fixes plus the dead-code
delete of the semantic LLM response cache.

POLICY_TYPES drift (W6 #5)
- lib/shared/schemas/governance.ts now includes
  'data_classification_notice' to match the Convex enum, killing the
  `as const` cast at use-data-classification-notice.ts:50.

documents/compare_documents.ts (W6 #8)
- Convex `_storage` is a global namespace; org membership alone was
  not enough to gate `ctx.storage.getUrl`. Adds a JOIN through
  fileMetadata via the new internal query verifyStorageIdsBelongToOrg
  to confirm both `baseStorageId` and `comparisonStorageId` are owned
  by the caller's org. Refuses with a clear error otherwise. Pattern
  copied from agent_tools/documents/helpers/retrieve_document.ts.

file_metadata/actions.ts::checkFileRagStatuses (W6 #9)
- Was an unauthenticated public action that could flip any org's
  fileMetadata.ragStatus to `failed` via expireStaleRagQueue (DoS,
  pre-existing on `main`). Now requires `getAuthUser` and filters
  storageIds to ones owned by an org the caller is a member of via
  the new file_metadata.internal_queries.filterStorageIdsByCallerOrg.

governance/queries.ts (W6 #11)
- getPolicy + listPolicies now apply a member-readable allow-list
  (data_classification_notice, feature_flags, pii_config,
  chat_filter, personalization, upload_policy, default_models). All
  other types — login_policy.trustedProxies, password_policy,
  two_factor_policy, model_access.rules, budgets, retention_policy,
  moderation_provider.endpoint, system_prompt — are admin-only.
  listPolicies silently filters those out for non-admins.

semantic LLM response cache — DELETE (W6 #12 + #13)
- Round-2 v05 confirmed the lookup is structurally cross-tenant
  (filters only on agent_name, model, expires_at, similarity; ignores
  user_id / organization_id even though they're stored). The platform
  helpers `lookupSemanticCache` / `storeSemanticCacheAsync` had ZERO
  callers in the monorepo, the FastAPI router was mounted but
  unreachable from platform — a latent foot-gun primed for the next
  dev to wire up unaware. Deletes:

  - services/platform/convex/lib/response_cache/semantic_cache.ts
  - services/platform/convex/lib/response_cache/internal_actions.ts
  - services/rag/app/routers/llm_cache.py
  - services/rag/app/services/llm_response_cache.py

  Plus the corresponding imports in routers/__init__.py, main.py,
  rag_service.py. Also removes the two empty-catch violations in
  semantic_cache.ts (no longer applicable).

The exact-key Convex `lib/response_cache/{internal_mutations,
internal_queries}.ts` cache stays — it is the actually-wired one and
is correctly org-scoped.
larryro added a commit that referenced this pull request May 9, 2026
…stodian cascade

Round-1 #10/#12/#13 + round-2 V3 confirmed three custodian-cascade gaps
(P0-6 retention guard, P0-7 deleteDocumentById, P0-8 cleanupMessageMetadata
+ cleanupChatFilterEvents). The user's observation made the deeper truth
clear: in production, only `org` and `userMembership` hold target types
are operator-facing — `thread` / `document` / `execution` were modeled in
the original fine-grained hold design but never wired into the place-hold
UI (`PICKER_TARGET_TYPES = ['userMembership', 'org']`), and `usePlaceLegalHold`
is called from a single dialog. The corresponding `holds.threadIds` /
`documentIds` / `executionIds` Sets in `ActiveHolds` therefore stayed
empty in every deployment, while ~80 LOC of dead per-row checks
proliferated through retention, erasure, restore, folders, and the
cascade helper. This commit collapses the model to its actual shape.

Schema-level narrowing (legal_hold.ts):
  - HOLD_TARGET_TYPES = ['userMembership', 'org'] (write-side)
  - ActiveHolds = { orgHeld, userMembershipIds }
  - loadActiveHolds switch reduces to 2 cases; legacy thread/document/
    execution rows in the table (if any) are intentionally ignored —
    they're surfaced in admin UI as `(legacy)` via the read-side
    validator in legal_hold_queries.ts.
  - resolveAndAssertTarget keeps only `org` + `userMembership` cases.
  - isHeld() simplified to org + custodian.

Guard simplification (legal_hold_guard.ts):
  - assertNotHeld drops the per-row branches; `targetType`/`targetId`
    are now error-message context only. The user-custodian cascade
    via `authorUserId` is the sole remaining gate beyond org-wide.

assertSafeRetentionDelete (internal_mutations_retention.ts):
  - Drops `targetType?` / `targetId?` args; adds `authorUserId?`.
  - Internal handler is now: cross-org check → TOCTOU check →
    org-wide hold → user-custodian cascade. Single, clear contract.

P0-6 cascade wiring — every retention mutation passes the row's
author user id (~15 call sites in internal_mutations_retention.ts):
  - documents → doc.createdBy
  - threads → thread.userId
  - workflowExecution → execution.userId
  - workflowTriggerLog → exec.userId via wfExecutionId join
  - messageFeedback → row.userId
  - memoryAudit → row.subjectUserId (with thread-fallback)
  - chatFilterEvent → parent thread's userId via index lookup
  - usageLedger → row.userId
  - promptTemplate → row.createdBy
  - messageMetadata → parent thread's userId (closes P0-8 directly)
  - tempFile, customer, vendor, externalConversation: org-only
    (no author concept on the row)

P0-8 cascade fix — cleanupMessageMetadata + cleanupChatFilterEvents
no longer rely on the now-empty `holds.threadIds` Set; both look up
the parent thread's `userId` for the user-membership cascade
(messageMetadata cleanup mutation was already P0-8 fixed in commit 1).

Other dead-check removals:
  - retention_cleanup.ts: ~10 dead `holds.threadIds.has` /
    `documentIds.has` / `executionIds.has` checks removed; per-row
    custodian cascade either replaces them or the mutation re-checks.
  - governance/restore.ts: drops dead per-row branches; user-custodian
    cascade in restoreSoftDeletedRow (commit 1) covers the rest.
  - threads/cascade_helpers.ts: per-thread hold check replaced with
    a thread-metadata lookup → `userMembershipIds.has(thread.userId)`.
  - threads/delete_chat_thread.ts + restore_chat_thread.ts: drop
    `holds.threadIds.has(threadId)`; ownerHeld + orgHeld cover all
    refusal cases.
  - folders/mutations.ts assertNoHeldDescendantDocs: per-document
    hold check replaced with `userMembershipIds.has(doc.createdBy)`.
  - governance/erasure.ts requestErasure: drops heldThreadIds /
    heldDocumentIds collection — refusal is now `orgHeld ||
    userCustodianHeld`. eraseSubjectDocuments drops the per-row
    held-document check (subject is by definition the only author
    in the iteration; user-custodian gate applied at request time).
  - legal_hold_internal.ts: loadActiveHoldsForOrg returns the
    narrowed ActiveHolds shape across the action/query boundary.

Tests rewritten — `governance/__tests__/retention_legal_hold_gaps.test.ts`:
  - Drops thread/document/execution per-row hold cases (behaviour
    deleted on purpose).
  - Adds 6 cases covering the simplified contract: authorUserId
    cascade, TOCTOU + cross-org pre-checks, missing authorUserId
    grandfather, org-wide hold blanket refusal.
  - Source-grep regressions: every retention mutation passes the
    right authorUserId; cleanup actions pre-filter via the right
    `holds.userMembershipIds.has(...)`; per-row Sets stay deleted
    from the cleanup / erasure / guard sources.

Note: this is a one-way migration. Any deployment with active
legacy thread/document/execution holds loses enforcement of those
holds going forward; the read-side validator in legal_hold_queries
keeps showing them as `(legacy)`. Acceptable per project memory
(demo stage, no prod use). Pre-prod check: convert to user/org
holds first if any deployment has them.

Verified: typecheck clean; 851 tests pass across affected dirs;
full vitest run shows 5641/5642 pass (1 pre-existing flake in
canvas-pane.test.tsx, unrelated).
larryro added a commit that referenced this pull request May 9, 2026
…te gates

P0-9 — Org-wide hold UI invisibility (round-1 #12, round-2 V4):
  Before: `heldByTargetValidator` declared `via: 'org'` but no producer
  ever emitted it. With only an org-wide hold active, `getLegalHoldByTarget`
  returned null and `listActiveHoldTargetIds` returned an empty list →
  no chat-sidebar lock icons, no admin-grid badges, no banner. Backend
  refused destructive actions but the UX showed nothing until the user
  attempted a delete and hit a confusing error.
  Fix:
  - `getLegalHoldByTarget`: third pass after direct + custodian-cascade
    looks up the org-wide hold row and surfaces it with `via: 'org'`.
  - `listActiveHoldTargetIds`: returns a new `orgHeld: boolean` flag
    alongside the per-target id set; the chat-sidebar ORs the flag with
    the per-thread set so every row gets the lock indicator when the
    whole org is on a hold.
  - `chat-history-sidebar.tsx`: replaces direct `heldThreadIds.has(id)`
    checks with an `isThreadHeld(id)` helper that ORs `orgWideHeld`.

P0-10 — `prepareOrganizationDeletion` bypasses holds (round-1 #13, V4):
  `cascadeOnOrgDeleted` hard-deletes userMemories + userPreferences (and
  per Commit 1, every thread-bound chat-upload file) for every member of
  the org. The mutation refused only on owner-role check; legal-hold
  status was never consulted. Adds `assertNotHeld(ctx, orgId, 'org',
  orgId, undefined, authUser._id)` before the cascade — refuses on org-wide
  hold OR on a userMembership cascade against the owner deleting.
  `GuardedTargetType` extended with `'org'` for the error-payload context.

P0-11 — `removeMember` bypasses custodian holds (round-1 #13, V4):
  `cascadeOnMemberRemoved` cascades into userMemories + userPreferences
  (+ chat-upload files) scoped to the member being removed. Mutation
  previously checked only role. Adds `assertNotHeld(ctx, orgId,
  'userMembership', member.userId, undefined, member.userId)` before
  the cascade — refuses when the org is on a hold OR when the member
  themselves is on a custodian hold. Self-author cascade uses the same
  user id for `targetId` and `authorUserId` so the gate fires regardless
  of which leg matches.

Verified: typecheck clean; 908 tests pass across affected dirs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant