Skip to content

fix: fixed some bugs and improved agent reasoning steps#1270

Open
Himanshvarma wants to merge 10 commits intomainfrom
fix/agentReasoningSteps
Open

fix: fixed some bugs and improved agent reasoning steps#1270
Himanshvarma wants to merge 10 commits intomainfrom
fix/agentReasoningSteps

Conversation

@Himanshvarma
Copy link
Collaborator

@Himanshvarma Himanshvarma commented Mar 8, 2026

Description

Testing

Additional Notes

Summary by CodeRabbit

Release Notes

  • New Features

    • Knowledge Base search with integrated file results and titles in global search
    • Enhanced reasoning visualization with improved streaming and merged view displays
    • Tool cooldown tracking and automatic recovery system
    • Response timing information displayed in reasoning headers
  • Improvements

    • Agentic mode now configurable via environment variable for flexibility
    • Claude model listings updated (Sonnet and Opus to version 4.6)
    • Search results merged by relevance for better quality outcomes

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive reasoning system overhaul with structured event-based telemetry, integrates Knowledge Base search and file results throughout the stack, implements tool cooldown management with turn-based recovery, adds environment-based agentic-by-default mode, and threads timing metadata through the chat pipeline.

Changes

Cohort / File(s) Summary
Reasoning System Refactor
frontend/src/components/EnhancedReasoning.tsx, frontend/src/components/ReasoningContext.tsx, frontend/src/components/StreamingReasoning.tsx, frontend/src/components/MergedReasoning.tsx, server/api/chat/reasoning-steps.ts
Replaces extensive internal reasoning step parsing with a ReasoningProvider-based context system. Introduces comprehensive typed factory functions for structured reasoning events, UI components (PlanCard, ReasoningStepComponent, ToolBlock, AgentBlock), and streaming/merged visualization branches. Exports buildReasoningTree and supporting types/constants for reasoning tree construction.
Knowledge Base Integration
frontend/src/components/ChatBox.tsx, frontend/src/components/SearchResult.tsx, frontend/src/components/Autocomplete.tsx, frontend/src/routes/_authenticated/search.tsx, server/api/search.ts, server/search/vespa.ts, server/vespa/schemas/ticket.sd, server/shared/types.ts
Adds KB file search and result rendering across frontend and backend. Extends SearchResult and Autocomplete to handle kb_items type, integrates parallel KB search in main search flow with result merging by docId, exposes searchVespaKnowledgeBase and groupVespaSearchKnowledgeBase APIs, adds new /search/knowledge-base route, and includes Vespa rank-profile for title-boosted hybrid search.
Tool Cooldown & Agentic Features
server/api/chat/tool-cooldown.ts, server/api/chat/message-agents.ts, frontend/src/routes/_authenticated/agent.tsx, frontend/src/routes/_authenticated/index.tsx
Introduces ToolCooldownManager for per-tool failure tracking and turn-based recovery. Implements VITE_AGENTIC_BY_DEFAULT environment flag for agentic mode defaults in routes and ChatBox, with conditional MCP/agent UI visibility. Updates message-agents to integrate cooldown state in tool instructions and emit structured reasoning events.
Telemetry & Streaming
frontend/src/hooks/useChatStream.ts, frontend/src/routes/_authenticated/chat.tsx, server/db/schema/messages.ts, server/db/message.ts
Adds timeTakenMs field to StreamState/StreamInfo with propagation from ResponseMetadata SSE events. Threads streamTimeTakenMs through ChatPage to VirtualizedMessages and ChatMessage for reasoning header display. Extends messages table schema with isSummary and timeTakenMs columns, adds getChatMessagesUntilCompaction for pre-compaction message retrieval, and filters summary messages across query functions.
Agent & Tool System Updates
server/api/chat/agents.ts, server/api/chat/chat.ts, server/api/chat/jaf-provider.ts, server/api/chat/tool-schemas.ts, server/api/chat/tools/slack/getSlackMessages.ts, server/api/chat/tools/utils.ts, server/api/chat/agent-schemas.ts, server/api/chat/utils.ts
Relaxes model ID validation to permit unmapped modelIds as pass-through, defaults missing providers to LiteLLM, migrates tool names from strings to XyneTools enum constants, updates tool-cooldown and reasoning-emitter signatures. Removes convertReasoningStepToText, adds collectReferencedFileIdsUntilCompaction for file deduplication, simplifies fragment IDs to use docId directly without chunk index.
Frontend Type & UI Extensions
frontend/src/components/ChatBox.tsx, frontend/src/lib/common.tsx, frontend/src/routes/_authenticated/ChatMessage.test.tsx
Extends ChatBoxProps with agentIdFromChatData and SearchResult with optional fileName for KB files. Adds KnowledgeBase "file" entity icon mapping (BookOpen). Updates lucide-react mock factory in ChatMessage.test.tsx to async override pattern for enhanced icon support.
Configuration & Model Updates
server/ai/context.ts, server/ai/fetchModels.ts, server/shared/types.ts
Replaces fixed maxSummaryChunks cap of 100 with dynamic value or 10 default for spreadsheet processing. Updates Claude model display names to 4.6 versions. Overhauls ReasoningStage/ReasoningEventType/ReasoningEventPayload types replacing legacy AgentReasoningStep variants, adds XyneTools enum updates (toDoWrite, searchGlobal, searchKnowledgeBase), and standardizes IngestionType values.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as ChatPage
    participant UseChatStream as useChatStream
    participant Backend as Server API
    participant Vespa as Vespa Search

    User->>Frontend: Submit chat query
    Frontend->>Backend: POST /api/chat (streamTimeTakenMs tracking)
    
    par Parallel Search Execution
        Backend->>Vespa: Main search (global/tools)
        Backend->>Vespa: KB search (searchVespaKnowledgeBase)
    and
        Backend->>Backend: Execute tools & agent logic
    end
    
    Vespa-->>Backend: Main results + KB results
    Backend->>Backend: Merge results by docId (prefer KB if higher relevance)
    
    Backend->>UseChatStream: Stream ReasoningEventPayload events (structured)
    UseChatStream->>UseChatStream: Parse & accumulate via ReasoningProvider
    UseChatStream-->>Frontend: timeTakenMs in ResponseMetadata
    
    Frontend->>Frontend: Render EnhancedReasoning with ReasoningContext
    alt isStreaming
        Frontend->>Frontend: Show StreamingReasoning (sliding window)
    else Post-Stream
        Frontend->>Frontend: Show MergedReasoning (full plan+steps)
    end
    
    Frontend-->>User: Display reasoning + merged search results + timeTakenMs
Loading
sequenceDiagram
    participant Agent as Message Agent
    participant ToolMgr as ToolCooldownManager
    participant Emitter as ReasoningEmitter
    participant Client as SSE Stream

    Agent->>ToolMgr: recoverExpiredTools(currentTurn)
    ToolMgr-->>Agent: List of recovered tools
    Agent->>Emitter: ReasoningSteps.toolRecovered(recovered)
    Emitter->>Client: Send ReasoningEventPayload
    
    Agent->>Agent: Select available tools (filtered by cooldown)
    Agent->>Emitter: ReasoningSteps.toolSelected(toolName, query)
    Emitter->>Client: Send ReasoningEventPayload
    
    Agent->>Agent: Execute tool
    alt Tool succeeds
        Agent->>Emitter: ReasoningSteps.toolCompleted(toolName, false)
        Emitter->>Client: Send success event
    else Tool fails
        Agent->>ToolMgr: recordFailure(toolName, error, currentTurn)
        ToolMgr->>ToolMgr: Increment count, check threshold
        alt Threshold reached
            ToolMgr->>Agent: Returns true (cooldown activated)
            Agent->>Emitter: ReasoningSteps.toolCooldownApplied(...)
        else Threshold not reached
            ToolMgr->>Agent: Returns false (still available)
            Agent->>Emitter: ReasoningSteps.toolCompleted(toolName, true)
        end
        Emitter->>Client: Send failure/cooldown event
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #1219: Modifies the same EnhancedReasoning/StreamingReasoning/MergedReasoning components and backend reasoning event types, directly aligned with this PR's reasoning system refactor.
  • #1157: Changes how reasoning SSE events are emitted in server/api/chat/agents.ts by centralizing structured reasoning payload emission instead of inline JSON, complementing this PR's reasoning-steps module.
  • #1007: Implements Knowledge Base frontend surface with KB file result types, collection/upload UI, and KB-specific search paths, directly related to this PR's KB integration feature.

Suggested labels

server, frontend, reasoning-system, knowledge-base, telemetry

Suggested reviewers

  • zereraz
  • shivamashtikar
  • kalpadhwaryu
  • junaid-shirur
  • devesh-juspay

Poem

🐰 A reasoning renaissance blooms,
With knowledge bases in every room,
Tools cooldown with turn-based grace,
While telemetry keeps its pace,
Agentic defaults set the scene—
The cleverest chat you've ever seen! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title is vague and does not accurately reflect the primary changes. The changeset includes knowledge base search improvements, agent reasoning restructuring, model selection updates, and numerous backend enhancements, but the title mentions only 'agent reasoning steps' and unspecified 'bugs'. Use a more specific title that captures the main change. Consider: 'feat: restructure reasoning visualization and add knowledge base search integration' or break into smaller, more focused PRs with clearer titles.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/agentReasoningSteps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the agentic chat experience by overhauling the reasoning display, introducing a structured event system for agent activity, and implementing a robust tool cooldown mechanism. These changes, alongside bug fixes for default agentic mode settings and improved attachment handling, aim to deliver a more reliable, performant, and user-friendly conversational AI.

Highlights

  • Agentic Mode Default Configuration: The application now uses the VITE_AGENTIC_BY_DEFAULT environment variable to set the default state of agentic mode, ensuring consistent behavior across different parts of the frontend and allowing for easier configuration.
  • Enhanced Reasoning UI Refactor: The agent reasoning display has undergone a major refactor, moving from a monolithic component to a modular architecture using React Context (ReasoningContext.tsx) and dedicated components for streaming (StreamingReasoning.tsx) and merged (MergedReasoning.tsx) views. This significantly improves maintainability, performance, and user experience during agent interactions.
  • Structured Reasoning Event System: The backend now emits structured reasoning events (reasoning-steps.ts) instead of free-form text. This new system provides a richer, more consistent, and easily parsable stream of agent activity, enabling a more detailed and accurate visualization on the frontend.
  • Tool Cooldown Mechanism: A new ToolCooldownManager has been implemented to introduce a cooldown period for tools that repeatedly fail. This mechanism prevents agents from getting stuck in infinite loops trying to use a broken tool, enhancing agent robustness and efficiency.
  • Improved Attachment and Document Handling: The logic for collecting and deduplicating referenced file IDs from chat history and attachments has been refined. It now prioritizes Vespa document IDs for more accurate deduplication, ensuring that the agent processes unique and relevant documents efficiently.
  • Model Name Correction: Corrected the display names for Claude Sonnet 4.6 and Opus 4.6 models in the model fetching logic.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • frontend/src/components/ChatBox.tsx
    • Introduced isAgenticByDefault constant based on VITE_AGENTIC_BY_DEFAULT environment variable.
    • Updated isAgenticMode prop default to use isAgenticByDefault.
    • Modified rendering conditions for the 'Dropdown for All Connectors' to hide it when isAgenticByDefault is true.
  • frontend/src/components/EnhancedReasoning.tsx
    • Removed extensive logic for parsing and rendering reasoning steps.
    • Imported ReasoningProvider, StreamingReasoning, and MergedReasoning components.
    • Updated EnhancedReasoning to act as a wrapper, providing context and conditionally rendering streaming or merged views.
    • Added timeTakenMs prop to display response generation time.
  • frontend/src/components/MergedReasoning.tsx
    • Added new component for displaying merged (post-stream) reasoning steps.
  • frontend/src/components/ReasoningContext.tsx
    • Added new React Context and provider for managing reasoning state, including parsing, progress, and icon mapping.
    • Defined new types for PlanInfo, ReasoningStep, ParsedReasoning, FlatItem, StepOrToolItem.
    • Centralized helper functions for generateStableId, getStageIcon, buildReasoningTree, isPlanningNoise, formatParametersInText, processReasoningWithCitations, buildToolGroupItems, buildFlatItems, parseReasoningEvent, parseReasoningContent, getCurrentProgressState.
    • Exported PlanCard, ReasoningStepComponent, ToolBlock, and AgentBlock for modular rendering.
  • frontend/src/components/StreamingReasoning.tsx
    • Added new component for displaying streaming reasoning steps with a sliding window and separate boxes for orchestrator and delegated agents.
  • frontend/src/hooks/useChatStream.ts
    • Added timeTakenMs to StreamState and StreamInfo interfaces to track response generation time.
    • Updated appendReasoningData to handle new structured reasoning event format.
    • Modified startStream to capture timeTakenMs from ResponseMetadata SSE event and pass it to the final message.
    • Updated useChatStream to also capture timeTakenMs.
  • frontend/src/routes/_authenticated/agent.tsx
    • Initialized isAgenticMode state using VITE_AGENTIC_BY_DEFAULT environment variable.
  • frontend/src/routes/_authenticated/chat.tsx
    • Updated isAgenticMode initialization to prioritize chatParams.agentic and fall back to VITE_AGENTIC_BY_DEFAULT.
    • Passed streamTimeTakenMs to ChatBox and VirtualizedMessages components.
  • frontend/src/routes/_authenticated/index.tsx
    • Updated isAgenticMode initialization to use VITE_AGENTIC_BY_DEFAULT if no stored value exists.
  • server/ai/context.ts
    • Adjusted maxSummaryChunks default value in answerContextMap to 10 if not explicitly provided.
  • server/ai/fetchModels.ts
    • Corrected display names for Claude Sonnet 4.6 and Opus 4.6 models.
  • server/api/chat/agent-schemas.ts
    • Imported ReasoningEventPayload type.
    • Added cooldownUntilTurn to ToolFailureInfo interface.
    • Updated emitReasoning callback type to ReasoningEventPayload.
    • Clarified comment for seenDocuments in AgentRunContext.
  • server/api/chat/agents.ts
    • Ensured actualModelId is set when an invalid model is encountered.
  • server/api/chat/chat.ts
    • Ensured actualModelId is set when an invalid model is encountered.
  • server/api/chat/jaf-provider.ts
    • Added fallback AIProviders.LiteLLM to ModelToProviderMap.
  • server/api/chat/message-agents.ts
    • Imported ToolCooldownManager, ReasoningSteps, emitReasoningEvent, StructuredReasoningEmitter.
    • Removed old streamReasoningStep and related reasoning helper functions.
    • Imported collectReferencedFileIdsUntilCompaction and getChatMessagesUntilCompaction.
    • Modified getFragmentDedupKey to prioritize Vespa docId for deduplication.
    • Updated mergeFragmentLists and recordFragmentsForContext to use the new deduplication key.
    • Added extractToolQuery function to provide human-readable tool queries for reasoning UI.
    • Replaced calls to streamReasoningStep with emitReasoningEvent(reasoningEmitter, ReasoningSteps.xxx(...)) for structured events.
    • Implemented tool cooldown logic using ToolCooldownManager in beforeToolExecutionHook and afterToolExecutionHook.
    • Adjusted targetChunks in prepareInitialAttachmentContext to 200.
    • Modified prepareInitialAttachmentContext to use SearchModes.GlobalSorted for ranking.
    • Updated afterToolExecutionHook to emit toolCompleted event and handle agent delegation IDs.
    • Changed tool names to use XyneTools enum.
    • Refined buildAgentInstructions to filter available tools by cooldown status and dynamically add delegation guidance.
    • Updated MessageAgents to use allReferencedFileIds from history, capture requestStartMs, and pass delegationRunId and reasoningEmitter to delegated agents.
    • Added timeTakenMs to message insertion and ResponseMetadata SSE event.
    • Removed redundant reasoning steps for tool execution start/end and turn end.
  • server/api/chat/reasoning-steps.ts
    • Added new file defining typed factory functions (ReasoningSteps) for all reasoning events.
    • Defined ReasoningEmitter and ReasoningEventPayload types.
    • Centralized TOOL_DISPLAY mapping for consistent tool names.
    • Implemented emitReasoningEvent as a wrapper for SSE emission.
  • server/api/chat/tool-cooldown.ts
    • Added new file defining ToolCooldownManager class for managing tool failures and cooldowns.
  • server/api/chat/tool-schemas.ts
    • Updated tool names to use XyneTools enum.
    • Modified getToolSchema and validateToolOutput to accept XyneTools enum.
  • server/api/chat/tools/slack/getSlackMessages.ts
    • Removed buildChunkFragmentId and used citation.docId directly as fragment ID.
  • server/api/chat/tools/utils.ts
    • Modified formatSearchToolResponse to use citation.docId as fragment ID.
  • server/api/chat/utils.ts
    • Removed AgentReasoningStepType and AgentReasoningStep types.
    • Added collectReferencedFileIdsUntilCompaction function to gather file IDs from messages.
    • Removed convertReasoningStepToText function.
  • server/db/message.ts
    • Added getChatMessagesUntilCompaction to fetch messages up to the last summary.
    • Modified getChatMessagesWithAuth, getChatMessagesBefore, getMessageCountsByChats, getMessageFeedbackStats, getMessagesWithAttachmentsByChatId, fetchUserQueriesForChat, fetchAgentQueryResponsePairs to exclude summary messages (isSummary: false).
  • server/db/schema/messages.ts
    • Added isSummary (boolean, default false) and timeTakenMs (integer) columns to the messages table.
  • server/shared/types.ts
    • Removed AgentToolName and AgentReasoningStepType enums.
    • Removed AgentReasoningStepEnhanced and all related AgentReasoningStep interfaces.
    • Introduced new ReasoningStage type and ReasoningEventType enum for structured reasoning events.
    • Defined PlanSubTask and ReasoningEventPayload interfaces for the new structured event system.
    • Updated XyneTools enum to reflect current tool names and removed deprecated ones.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a major refactoring of the agent reasoning and streaming architecture, moving from a freeform text-based system to a structured event-based one. This is a significant improvement for maintainability and frontend user experience. The changes also include a new tool failure cooldown mechanism, more robust context handling, and various bug fixes. My review focuses on a potentially impactful change to context chunking.

Note: Security Review did not run due to the size of the PR.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (7)
server/ai/fetchModels.ts (1)

324-327: ⚠️ Potential issue | 🟡 Minor

Update log messages to match the new model version.

The log messages still reference "Claude Sonnet 4.5" and "Claude Opus 4.5", which is inconsistent with the updated display names "Claude Sonnet 4.6" and "Claude Opus 4.6" at lines 206 and 210.

📝 Proposed fix to align log messages
                if (isSonnet46(id) && config.allowSonnet46) {
-                    Logger.info("Allowing Claude Sonnet 4.5 model despite litellm_provider not being 'hosted_vllm'")
+                    Logger.info("Allowing Claude Sonnet 4.6 model despite litellm_provider not being 'hosted_vllm'")
                }
                if (isOpus46(id) && config.allowOpus46) {
-                    Logger.info("Allowing Claude Opus 4.5 model despite litellm_provider not being 'hosted_vllm'")
+                    Logger.info("Allowing Claude Opus 4.6 model despite litellm_provider not being 'hosted_vllm'")
                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/ai/fetchModels.ts` around lines 324 - 327, The log messages
referencing "Claude Sonnet 4.5" and "Claude Opus 4.5" are outdated; update the
Logger.info calls that occur when allowing these models (the branches using the
Sonnet/Opus checks, e.g., the isOpus46(id) check and the corresponding Sonnet
check) to use the new display names "Claude Sonnet 4.6" and "Claude Opus 4.6" so
they match the display name updates at the model definition lines.
server/db/message.ts (1)

276-283: ⚠️ Potential issue | 🟠 Major

Exclude soft-deleted rows from the detailed feedback list.

This query still omits deletedAt IS NULL, while the aggregate query above includes it. A soft-deleted message will disappear from the totals but still show up in feedbackMessages, which makes the stats inconsistent and can resurface deleted feedback text.

Suggested fix
     .where(
       and(
         inArray(messages.chatExternalId, chatExternalIds),
         eq(messages.email, email),
         eq(messages.workspaceExternalId, workspaceExternalId),
+        isNull(messages.deletedAt),
         eq(messages.isSummary, false),
         sql`${messages.feedback}->>'type' IN ('like', 'dislike')`,
       ),
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/db/message.ts` around lines 276 - 283, The feedbackMessages query is
missing a filter to exclude soft-deleted rows, causing deleted messages to
appear in the detailed feedback list; update the .where(...) clause that builds
feedbackMessages to also require messages.deletedAt IS NULL (e.g. add
eq(messages.deletedAt, null) or the equivalent isNull(messages.deletedAt)
condition alongside inArray(messages.chatExternalId, ...), eq(messages.email,
...), eq(messages.workspaceExternalId, ...), eq(messages.isSummary, false), and
the existing sql feedback type check) so the detailed list matches the aggregate
totals and omits soft-deleted feedback.
server/api/chat/tools/slack/getSlackMessages.ts (1)

206-221: ⚠️ Potential issue | 🟠 Major

Address potential fragment deduplication issues with duplicate docIds.

The change from buildChunkFragmentId(citation.docId, idx) to citation.docId removes index-based uniqueness. Fragment IDs are used for set-based deduplication downstream (message-agents.ts:1872), and if allItems contains duplicate docIds (possible when thread root messages appear in both initial search results and thread fetches), legitimate fragments will be incorrectly filtered out.

Ensure docId uniqueness in allItems before fragment creation, or restore the index-based uniqueness guarantee.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/tools/slack/getSlackMessages.ts` around lines 206 - 221, The
fragment ID change replaced buildChunkFragmentId(citation.docId, idx) with
citation.docId which breaks downstream set-based deduplication (see
message-agents.ts:1872) when allItems contains duplicate docIds; restore
uniqueness by either (A) deduplicating allItems by docId before mapping to
MinimalAgentFragment (use searchToCitation(item).docId to collapse duplicates)
or (B) restore index-based uniqueness when creating fragments (use
buildChunkFragmentId(citation.docId, idx) or append the item's index) in the
fragment creation block that calls searchToCitation and answerContextMap so
fragments.id remains unique for allItems.
frontend/src/components/ChatBox.tsx (1)

914-931: ⚠️ Potential issue | 🔴 Critical

Critical: errorMessage used before declaration causes runtime error.

Line 921 references errorMessage, but it's not defined until line 929. This will throw a ReferenceError due to the temporal dead zone when an upload is aborted.

🐛 Proposed fix
           } catch (error) {
             if (error instanceof Error && error.name === "AbortError") {
+              const abortMessage = "Upload cancelled"
               setSelectedFiles((prev) =>
                 prev.map((f) =>
                   f.id === selectedFile.id
                     ? {
                         ...f,
                         uploading: false,
-                        uploadError: errorMessage,
+                        uploadError: abortMessage,
                       }
                     : f,
                 ),
               )
               return null
             }
             const errorMessage =
               error instanceof Error ? error.message : "Upload failed"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ChatBox.tsx` around lines 914 - 931, The catch block
in ChatBox.tsx uses errorMessage before it's declared which causes a runtime
ReferenceError; fix by computing the errorMessage immediately after entering the
catch (e.g., declare const errorMessage = error instanceof Error ? error.message
: "Upload failed") and then handle the AbortError branch using that variable
when calling setSelectedFiles for the file with id selectedFile.id (the
setSelectedFiles update that flips uploading to false and sets uploadError).
Ensure the error instanceof Error check is reused (or repeated) as needed and
remove the later declaration so no duplicate definitions remain.
server/api/chat/message-agents.ts (2)

1870-1873: ⚠️ Potential issue | 🟠 Major

Filter previously seen contexts with the same dedup key you store.

gatheredFragmentsKeys is populated with getFragmentDedupKey(), but this filter still checks c.id. When the same Vespa doc comes back under a different synthetic fragment id, it survives filteredContexts, burns ranking tokens, and can crowd out genuinely new docs before addToolFragments() drops it later.

Suggested fix
-    const filteredContexts = contexts.filter(
-      (c: MinimalAgentFragment) => !gatheredFragmentsKeys.has(c.id)
-    )
+    const filteredContexts = contexts.filter((c: MinimalAgentFragment) => {
+      const key = getFragmentDedupKey(c)
+      return !key || !gatheredFragmentsKeys.has(key)
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/message-agents.ts` around lines 1870 - 1873, The filter is
using c.id but gatheredFragmentsKeys contains dedup keys from
getFragmentDedupKey(), so replace the membership check to compare
getFragmentDedupKey(c) against gatheredFragmentsKeys (or precompute a set of
keys for contexts and filter by that) so previously-seen fragments are correctly
excluded; update the filteredContexts creation (where contexts,
MinimalAgentFragment and gatheredFragmentsKeys are referenced) to call
getFragmentDedupKey for each context instead of using c.id so duplicates by
dedup key are filtered before addToolFragments runs.

1643-1654: ⚠️ Potential issue | 🟡 Minor

Don't emit a "skipped" validation step unless you actually skip.

This branch now sends toolValidationError, but the hook explicitly continues into normal execution. The UI will say "Skipping this step due to invalid input." even when the tool still runs and may succeed or fail later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/message-agents.ts` around lines 1643 - 1654, The code emits
ReasoningSteps.toolValidationError (via emitReasoningEvent) even though
execution continues; change this to a non-skipping event or only emit the
"error/skipped" event when you actually abort execution. Specifically, in the
validateToolInput branch referencing validateToolInput, emitReasoningEvent with
a new/alternate step (e.g., ReasoningSteps.toolValidationWarning or similar)
that indicates a validation warning, or move the
ReasoningSteps.toolValidationError call to the branch where you actually
skip/return; keep Logger.warn as-is but do not send the "skipped" error event
unless the tool is truly skipped. Ensure the code updates use
emitReasoningEvent, ReasoningSteps.toolValidationError, and validateToolInput
identifiers when making the change.
server/shared/types.ts (1)

744-749: ⚠️ Potential issue | 🟡 Minor

Reject invalid createdAt strings instead of transforming them into Invalid Date.

new Date("bad") still returns a Date object, so malformed attachment metadata will pass this schema and fail later on serialization or comparison.

Suggested fix
-  createdAt: z.union([z.string(), z.date()]).transform((val) => {
-    if (typeof val === "string") {
-      return new Date(val)
-    }
-    return val
-  }),
+  createdAt: z.preprocess(
+    (val) =>
+      val instanceof Date || typeof val === "string" ? new Date(val) : val,
+    z.date().refine(
+      (date) => !Number.isNaN(date.getTime()),
+      "Invalid createdAt",
+    ),
+  ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/shared/types.ts` around lines 744 - 749, The createdAt schema
currently transforms any string via new Date(...) which allows invalid dates;
update the createdAt handling to explicitly validate parsed dates and reject bad
strings instead of returning Invalid Date. In the createdAt transform (in
server/shared/types.ts) parse the string into a Date, check validity via
isNaN(parsed.getTime()) (or similar), and if invalid cause the Zod validation to
fail (e.g., throw or return an error/refine), otherwise return the valid Date;
alternatively use z.preprocess or z.string().refine + z.date() to enforce and
reject malformed date strings before producing a Date object.
🧹 Nitpick comments (4)
server/api/chat/utils.ts (1)

154-202: Minor redundancy in deduplication logic.

The seen Set already prevents duplicates from being added to fileIds, making the Array.from(new Set(fileIds)) calls at lines 173, 184, 195, and 201 redundant. Consider simplifying to just fileIds.slice(0, maxFiles).

♻️ Optional simplification
-          if (fileIds.length >= maxFiles) return Array.from(new Set(fileIds)).slice(0, maxFiles)
+          if (fileIds.length >= maxFiles) return fileIds.slice(0, maxFiles)

Apply similar change to lines 184, 195, and 201.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/utils.ts` around lines 154 - 202, In
collectReferencedFileIdsUntilCompaction, the seen Set already prevents
duplicates so replace the redundant Array.from(new Set(fileIds)).slice(0,
maxFiles) early returns (inside the attachments, fileIds, and sources loops) and
the final return with simple fileIds.slice(0, maxFiles); ensure all early-return
sites that currently call Array.from(new Set(fileIds)).slice(...) instead return
fileIds.slice(0, maxFiles) so deduplication is only handled by seen and the
result is still trimmed to MAX_FILES.
frontend/src/components/StreamingReasoning.tsx (2)

104-113: Clarify the hardcoded index={0} usage.

The index prop is hardcoded to 0 for all items. If this is intentional (perhaps the index isn't used for display purposes), consider adding a comment. Otherwise, if the index matters for styling or behavior, consider using the actual index from visibleMainItems.

📝 Option 1: Add clarifying comment
                   <ReasoningStepComponent
                     step={item.step}
-                    index={0}
+                    index={0} // index not used for display; only step content matters
                     isStreaming={isStreaming}
📝 Option 2: Use actual index if needed
-            {visibleMainItems.map((item) => (
+            {visibleMainItems.map((item, idx) => (
               <div key={item.key} className="reasoning-step-enter">
                 {item.kind === "tool" ? (
                   ...
                 ) : (
                   <ReasoningStepComponent
                     step={item.step}
-                    index={0}
+                    index={idx}
                     isStreaming={isStreaming}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/StreamingReasoning.tsx` around lines 104 - 113, The
ReasoningStepComponent call is passing a hardcoded index={0} which is ambiguous;
either add a short inline comment explaining index is intentionally constant
(e.g., "index not used for display/ordering") or change it to use the actual
loop index from visibleMainItems (e.g., pass the iterator index variable) so
styling/behavior depending on index works correctly; update the JSX where
ReasoningStepComponent is rendered to reference the real index or add the
clarifying comment next to the index prop to make the intent explicit.

62-70: Move CSS keyframes outside the render function.

The inline <style> tag is recreated on every render. Consider moving these keyframes to a CSS file, a global stylesheet, or using a CSS-in-JS solution with proper caching.

♻️ Move to a CSS module or global styles

Create a CSS file or add to existing global styles:

/* In a CSS file or global styles */
`@keyframes` rsStepIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.reasoning-step-enter {
  animation: rsStepIn 0.18s ease-out forwards;
}

Then remove the inline <style> block from the component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/StreamingReasoning.tsx` around lines 62 - 70, The
inline <style> block inside the StreamingReasoning component recreates the
`@keyframes` rsStepIn and .reasoning-step-enter rules on every render; move those
rules out of the render into a CSS file (or CSS module/global stylesheet or a
cached CSS-in-JS solution) by adding the `@keyframes` rsStepIn and
.reasoning-step-enter animation to your stylesheet and then remove the inline
<style> tag from StreamingReasoning.tsx so the component simply uses the
.reasoning-step-enter class without redefining keyframes each render.
frontend/src/components/MergedReasoning.tsx (1)

30-33: Consider scrollbar visibility for accessibility.

Hiding the scrollbar entirely (scrollbarWidth: "none") may impact users who rely on visible scrollbars for navigation cues. Consider using a subtle scrollbar or keeping the default behavior.

♿ Optional: Use subtle scrollbar instead of hiding
       <div
         className="max-h-80 overflow-y-auto pr-1"
-        style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
+        style={{ scrollbarWidth: "thin" }}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/MergedReasoning.tsx` around lines 30 - 33, The
current div in the MergedReasoning component that uses className "max-h-80
overflow-y-auto pr-1" hides the scrollbar via inline styles (scrollbarWidth:
"none", msOverflowStyle: "none"), which reduces accessibility; instead, remove
those inline styles and either keep the default scrollbar or implement a subtle,
accessible scrollbar (e.g., use CSS rules or a Tailwind utility to make the
scrollbar thin/low-contrast or add ::-webkit-scrollbar styles) so users still
get a visible scroll cue while preserving the overflow-y-auto behavior in the
MergedReasoning component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/ReasoningContext.tsx`:
- Around line 211-235: The current buildToolGroupItems (and similarly
buildFlatItems) groups all steps by toolExecutionId across the entire array,
which pulls later non-contiguous occurrences forward; change the algorithm to
group only contiguous runs: iterate the steps in order, maintain a current block
state (currentToolId/currentDelegationId) and accumulate steps while the id
stays the same, flushing the block to items when the id changes or when a
non-tool step is encountered; when you flush, emit a single "tool" item with the
accumulated steps and key derived from that block (use the first step's
id/timestamp), and for non-tool steps emit individual "step" items; update
buildToolGroupItems to stop using the global toolBlocks map and implement the
single-pass contiguous grouping, and apply the same contiguous grouping logic to
buildFlatItems for delegationRunId and toolExecutionId.
- Around line 579-583: The component currently mounts expanded then collapses in
useEffect, causing a flash; initialize the expanded state from the isStreaming
prop and synchronize on changes instead of correcting after paint. Replace
useState(true) with useState initialized from isStreaming (e.g., useState(() =>
isStreaming)) and update the effect to setExpanded(isStreaming) (or simply
remove the effect and drive expansion directly from isStreaming changes) so the
memoized component (the ToolBlock-like component using expanded and setExpanded)
starts in the correct state and updates when isStreaming changes.
- Around line 404-417: The fallback logic only scans top-level steps for a stage
and misses earlier substeps inside the latest turn, so update the search to walk
substeps backward first: when computing latestStage (using variables lastTop,
lastTop.substeps, latestStage and enum ReasoningEventType.TurnStarted), if
lastTop.substeps exists scan its elements from end to start to find the first
substep with a .stage and use that; if none found then fall back to
lastTop.stage and finally do the existing backwards scan over steps to find a
stage. Ensure you reference lastTop.substeps[lastTop.substeps.length - 1]
replacement with a reverse loop over substeps before the top-level fallback.
- Around line 163-169: The regex in const parameterPattern is too permissive
because the case-insensitive flag makes the lookahead \n[A-Z] match lowercase
and prematurely end multi-line key/value blocks; update parameterPattern to
remove the case-insensitive 'i' flag and tighten the lookahead to a real section
boundary (e.g., require a blank line OR a newline followed by an uppercase
header token and a colon like "HeaderName:"), so the match for Parameters:
captures multi-line key/value pairs (e.g., "query: ...\nlimit: ...") until an
actual section boundary or EOF; locate parameterPattern in ReasoningContext.tsx
and replace the pattern accordingly.

In `@frontend/src/routes/_authenticated/chat.tsx`:
- Around line 228-232: The Zod schema for agentic currently forces
chatParams.agentic to always be boolean (due to .default("false")), so the state
init in isAgenticMode (which checks chatParams.agentic !== undefined and falls
back to import.meta.env.VITE_AGENTIC_BY_DEFAULT) never hits the fallback; fix by
either 1) updating the Zod schema for the agentic field (remove or change
.default("false") so agentic can be undefined/preserved) so chatParams.agentic
can be undefined and the existing isAgenticMode logic works, or 2) keep the
schema but change the initialization in
frontend/src/routes/_authenticated/chat.tsx to derive the sentinel from the raw
URL search params (e.g., check URLSearchParams.get('agentic') before Zod
parsing) and then decide the default using
import.meta.env.VITE_AGENTIC_BY_DEFAULT; target symbols: agentic Zod schema,
chatParams.agentic, isAgenticMode / setIsAgenticMode, and
VITE_AGENTIC_BY_DEFAULT.

In `@frontend/src/routes/_authenticated/index.tsx`:
- Around line 39-41: Guard parsing of the persisted AGENTIC_STATE: when reading
storedValue from localStorage (variable storedValue), wrap JSON.parse in a
try/catch and on any parse error or if the parsed result is not a boolean
(typeof result !== "boolean") return the fallback value
import.meta.env.VITE_AGENTIC_BY_DEFAULT === "true"; ensure only a boolean
payload is accepted and any malformed or non-boolean stored value falls back to
the env default.

In `@server/api/chat/agents.ts`:
- Around line 1188-1191: The log message claims the code is "Using default" but
actualModelId is being set to the invalid modelId; update the log to reflect the
real behavior (e.g., `Invalid model: ${modelId}. Retaining provided modelId`) so
it matches the assignment, or alternatively make the code match the message by
setting actualModelId to the desired default constant instead; locate the
assignment to actualModelId and the loggerWithChild({...}).error call and either
change the error text to indicate retention of modelId or change the assignment
to use the default model constant from your Models enum/label mappings.

In `@server/api/chat/chat.ts`:
- Around line 5048-5051: The code is incorrectly assigning an invalid modelId
into actualModelId (actualModelId = modelId) which prevents the later fallback
(actualModelId || config.defaultBestModel) from using the default; remove that
assignment (or set actualModelId = undefined/null) in the branch that logs the
invalid model so actualModelId remains falsy and the fallback
config.defaultBestModel is used; keep the loggerWithChild(...).error call as-is
to record the invalid model.

In `@server/api/chat/message-agents.ts`:
- Around line 4062-4068: When reconstructing agentContext you currently merge
referencedFileIds with historyFileIds expanded via expandSheetIds but only
source image attachments from the current request's attachmentMetadata; collect
historical image/file attachment IDs from previousConversationHistory (e.g.,
parse prior turns' attachmentMetadata or call
collectReferencedFileIdsUntilCompaction on previousConversationHistory including
image types) and include those IDs in the same Set used to build
allReferencedFileIds so prior images are available during context rebuild;
update the same logic used around collectReferencedFileIdsUntilCompaction,
expandSheetIds and wherever agentContext is rebuilt (also apply the same change
at the other similar block that uses referencedFileIds/attachmentMetadata) so
historical image attachments are rehydrated into agentContext.
- Around line 2320-2327: The toolCompleted event is using hookContext.status to
decide failure but earlier the code normalizes non-"success" into record.status
= "error", causing mismatch; update the emit call in the block that calls
emitReasoningEvent with ReasoningSteps.toolCompleted to use the normalized
status (e.g., record.status or the normalizedStatus variable) instead of
hookContext.status so the emitted toolCompleted reflects the same normalized
error state used elsewhere.

In `@server/api/chat/reasoning-steps.ts`:
- Around line 80-84: The displayText in turnStarted
(ReasoningEventType.TurnStarted) assumes 1-based turns but MessageAgents uses
0-based turns, so update turnStarted to treat turnNumber === 0 as the first turn
(use "Starting research.") and for subsequent turns compute a human-facing pass
number by using turnNumber + 1 in the "Starting search pass ..." message; adjust
the conditional logic in the turnStarted function accordingly.

In `@server/db/message.ts`:
- Around line 57-90: getChatMessagesUntilCompaction currently returns the
summary row and uses only createdAt as a boundary, causing correctness and race
issues; change it to fetch the summary row's id (use lastSummary[0].id), exclude
summary messages (isSummary = false) in the subsequent query, and replace the
single-column cutoff with a stable two-column boundary by returning rows where
(createdAt < cutoffCreatedAt) OR (createdAt = cutoffCreatedAt AND id <=
cutoffId) so ties are broken by id; keep the early-return to
getChatMessagesWithAuth when lastSummary is empty and still parse the final
result with selectMessageSchema.

---

Outside diff comments:
In `@frontend/src/components/ChatBox.tsx`:
- Around line 914-931: The catch block in ChatBox.tsx uses errorMessage before
it's declared which causes a runtime ReferenceError; fix by computing the
errorMessage immediately after entering the catch (e.g., declare const
errorMessage = error instanceof Error ? error.message : "Upload failed") and
then handle the AbortError branch using that variable when calling
setSelectedFiles for the file with id selectedFile.id (the setSelectedFiles
update that flips uploading to false and sets uploadError). Ensure the error
instanceof Error check is reused (or repeated) as needed and remove the later
declaration so no duplicate definitions remain.

In `@server/ai/fetchModels.ts`:
- Around line 324-327: The log messages referencing "Claude Sonnet 4.5" and
"Claude Opus 4.5" are outdated; update the Logger.info calls that occur when
allowing these models (the branches using the Sonnet/Opus checks, e.g., the
isOpus46(id) check and the corresponding Sonnet check) to use the new display
names "Claude Sonnet 4.6" and "Claude Opus 4.6" so they match the display name
updates at the model definition lines.

In `@server/api/chat/message-agents.ts`:
- Around line 1870-1873: The filter is using c.id but gatheredFragmentsKeys
contains dedup keys from getFragmentDedupKey(), so replace the membership check
to compare getFragmentDedupKey(c) against gatheredFragmentsKeys (or precompute a
set of keys for contexts and filter by that) so previously-seen fragments are
correctly excluded; update the filteredContexts creation (where contexts,
MinimalAgentFragment and gatheredFragmentsKeys are referenced) to call
getFragmentDedupKey for each context instead of using c.id so duplicates by
dedup key are filtered before addToolFragments runs.
- Around line 1643-1654: The code emits ReasoningSteps.toolValidationError (via
emitReasoningEvent) even though execution continues; change this to a
non-skipping event or only emit the "error/skipped" event when you actually
abort execution. Specifically, in the validateToolInput branch referencing
validateToolInput, emitReasoningEvent with a new/alternate step (e.g.,
ReasoningSteps.toolValidationWarning or similar) that indicates a validation
warning, or move the ReasoningSteps.toolValidationError call to the branch where
you actually skip/return; keep Logger.warn as-is but do not send the "skipped"
error event unless the tool is truly skipped. Ensure the code updates use
emitReasoningEvent, ReasoningSteps.toolValidationError, and validateToolInput
identifiers when making the change.

In `@server/api/chat/tools/slack/getSlackMessages.ts`:
- Around line 206-221: The fragment ID change replaced
buildChunkFragmentId(citation.docId, idx) with citation.docId which breaks
downstream set-based deduplication (see message-agents.ts:1872) when allItems
contains duplicate docIds; restore uniqueness by either (A) deduplicating
allItems by docId before mapping to MinimalAgentFragment (use
searchToCitation(item).docId to collapse duplicates) or (B) restore index-based
uniqueness when creating fragments (use buildChunkFragmentId(citation.docId,
idx) or append the item's index) in the fragment creation block that calls
searchToCitation and answerContextMap so fragments.id remains unique for
allItems.

In `@server/db/message.ts`:
- Around line 276-283: The feedbackMessages query is missing a filter to exclude
soft-deleted rows, causing deleted messages to appear in the detailed feedback
list; update the .where(...) clause that builds feedbackMessages to also require
messages.deletedAt IS NULL (e.g. add eq(messages.deletedAt, null) or the
equivalent isNull(messages.deletedAt) condition alongside
inArray(messages.chatExternalId, ...), eq(messages.email, ...),
eq(messages.workspaceExternalId, ...), eq(messages.isSummary, false), and the
existing sql feedback type check) so the detailed list matches the aggregate
totals and omits soft-deleted feedback.

In `@server/shared/types.ts`:
- Around line 744-749: The createdAt schema currently transforms any string via
new Date(...) which allows invalid dates; update the createdAt handling to
explicitly validate parsed dates and reject bad strings instead of returning
Invalid Date. In the createdAt transform (in server/shared/types.ts) parse the
string into a Date, check validity via isNaN(parsed.getTime()) (or similar), and
if invalid cause the Zod validation to fail (e.g., throw or return an
error/refine), otherwise return the valid Date; alternatively use z.preprocess
or z.string().refine + z.date() to enforce and reject malformed date strings
before producing a Date object.

---

Nitpick comments:
In `@frontend/src/components/MergedReasoning.tsx`:
- Around line 30-33: The current div in the MergedReasoning component that uses
className "max-h-80 overflow-y-auto pr-1" hides the scrollbar via inline styles
(scrollbarWidth: "none", msOverflowStyle: "none"), which reduces accessibility;
instead, remove those inline styles and either keep the default scrollbar or
implement a subtle, accessible scrollbar (e.g., use CSS rules or a Tailwind
utility to make the scrollbar thin/low-contrast or add ::-webkit-scrollbar
styles) so users still get a visible scroll cue while preserving the
overflow-y-auto behavior in the MergedReasoning component.

In `@frontend/src/components/StreamingReasoning.tsx`:
- Around line 104-113: The ReasoningStepComponent call is passing a hardcoded
index={0} which is ambiguous; either add a short inline comment explaining index
is intentionally constant (e.g., "index not used for display/ordering") or
change it to use the actual loop index from visibleMainItems (e.g., pass the
iterator index variable) so styling/behavior depending on index works correctly;
update the JSX where ReasoningStepComponent is rendered to reference the real
index or add the clarifying comment next to the index prop to make the intent
explicit.
- Around line 62-70: The inline <style> block inside the StreamingReasoning
component recreates the `@keyframes` rsStepIn and .reasoning-step-enter rules on
every render; move those rules out of the render into a CSS file (or CSS
module/global stylesheet or a cached CSS-in-JS solution) by adding the
`@keyframes` rsStepIn and .reasoning-step-enter animation to your stylesheet and
then remove the inline <style> tag from StreamingReasoning.tsx so the component
simply uses the .reasoning-step-enter class without redefining keyframes each
render.

In `@server/api/chat/utils.ts`:
- Around line 154-202: In collectReferencedFileIdsUntilCompaction, the seen Set
already prevents duplicates so replace the redundant Array.from(new
Set(fileIds)).slice(0, maxFiles) early returns (inside the attachments, fileIds,
and sources loops) and the final return with simple fileIds.slice(0, maxFiles);
ensure all early-return sites that currently call Array.from(new
Set(fileIds)).slice(...) instead return fileIds.slice(0, maxFiles) so
deduplication is only handled by seen and the result is still trimmed to
MAX_FILES.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f7e9b65-72aa-4dba-b800-fc4780555e23

📥 Commits

Reviewing files that changed from the base of the PR and between f16ea5e and dfd1b90.

📒 Files selected for processing (25)
  • frontend/src/components/ChatBox.tsx
  • frontend/src/components/EnhancedReasoning.tsx
  • frontend/src/components/MergedReasoning.tsx
  • frontend/src/components/ReasoningContext.tsx
  • frontend/src/components/StreamingReasoning.tsx
  • frontend/src/hooks/useChatStream.ts
  • frontend/src/routes/_authenticated/agent.tsx
  • frontend/src/routes/_authenticated/chat.tsx
  • frontend/src/routes/_authenticated/index.tsx
  • server/ai/context.ts
  • server/ai/fetchModels.ts
  • server/api/chat/agent-schemas.ts
  • server/api/chat/agents.ts
  • server/api/chat/chat.ts
  • server/api/chat/jaf-provider.ts
  • server/api/chat/message-agents.ts
  • server/api/chat/reasoning-steps.ts
  • server/api/chat/tool-cooldown.ts
  • server/api/chat/tool-schemas.ts
  • server/api/chat/tools/slack/getSlackMessages.ts
  • server/api/chat/tools/utils.ts
  • server/api/chat/utils.ts
  • server/db/message.ts
  • server/db/schema/messages.ts
  • server/shared/types.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
server/api/chat/message-agents.ts (1)

5364-5389: ⚠️ Potential issue | 🟠 Major

Record turn 0 expectations before review runs.

currentTurn > 0 now skips persistence for the first turn, but turn_start has already happened, so nothing flushes that buffer before the same turn's tool-error or turn-end review. Turn 0 reviews therefore miss the expected_results they are supposed to evaluate.

Suggested fix
-                  if (currentTurn > 0) {
+                  if (currentTurn >= MIN_TURN_NUMBER) {

Apply the same change in the delegated-agent path.

Also applies to: 6598-6609

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/message-agents.ts` around lines 5364 - 5389, The first-turn
expectations are being buffered instead of persisted which causes turn 0 reviews
to miss expected_results; update the delegated-agent path to mirror the
main-agent change so that whenever extractedExpectations are collected you push
them into pendingExpectations and agentContext.currentTurnArtifacts.expectations
and then call recordExpectationsForTurn(currentTurn, extractedExpectations) when
currentTurn >= 0 (i.e., ensure turn 0 is recorded rather than only currentTurn >
0), otherwise push to expectationBuffer only for genuinely future turns; apply
this same fix in the delegated-agent block referenced around the other
occurrence (the block around lines 6598-6609) so both paths use the same
currentTurn handling and calls to recordExpectationsForTurn, Logger.debug,
pendingExpectations, expectationBuffer, and
agentContext.currentTurnArtifacts.expectations.
♻️ Duplicate comments (2)
server/api/chat/message-agents.ts (1)

4062-4068: ⚠️ Potential issue | 🟠 Major

Carry forward prior image attachments when rebuilding context.

This merge only rehydrates historical non-image file ids. Follow-up questions about an image uploaded in an earlier turn still rebuild initialAttachmentContext without that image, while prior documents remain available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/message-agents.ts` around lines 4062 - 4068, The current
rebuild only adds expanded sheet ids from history, dropping prior image
attachments; update the aggregation that builds allReferencedFileIds so it
carries forward raw historical file ids (historyFileIds) in addition to sheet
expansions. Specifically, in the block that computes allReferencedFileIds (which
uses referencedFileIds, historyFileIds, and expandSheetIds), include
historyFileIds directly (not only historyFileIds.flatMap(...)) so image file ids
from collectReferencedFileIdsUntilCompaction are preserved when reconstructing
initialAttachmentContext.
frontend/src/components/ReasoningContext.tsx (1)

404-423: ⚠️ Potential issue | 🟡 Minor

Keep scanning older turn substeps before defaulting the progress banner.

If the newest turn has no staged substep, the fallback still only checks steps[i].stage. That can reset the banner to "Understanding your query..." even though an earlier turn already reached gathering or analyzing.

🔎 Tight fallback fix
   latestStage ??= lastTop.stage
   if (!latestStage) {
-    for (let i = steps.length - 1; i >= 0; i--) {
-      if (steps[i].stage) {
-        latestStage = steps[i].stage
-        break
-      }
-    }
+    for (let i = steps.length - 1; i >= 0 && !latestStage; i--) {
+      const step = steps[i]
+      if (step.type === ReasoningEventType.TurnStarted && step.substeps?.length) {
+        for (let j = step.substeps.length - 1; j >= 0; j--) {
+          if (step.substeps[j].stage) {
+            latestStage = step.substeps[j].stage
+            break
+          }
+        }
+      }
+      latestStage ??= step.stage
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ReasoningContext.tsx` around lines 404 - 423, The
fallback only checks steps[i].stage and misses staged substeps in older turns,
so update the fallback scanning logic (after latestStage ??= lastTop.stage) to
also inspect each steps[i].substeps for a stage (mirroring the earlier loop over
lastTop.substeps) before falling back to steps[i].stage; specifically, in the
for loop that iterates steps from the end, check steps[i].substeps (if present)
from last to first for a .stage and set latestStage to that stage, otherwise
fall back to steps[i].stage, ensuring STAGE_PROGRESS_TEXT[latestStage] uses the
most recent substep stage when present.
🧹 Nitpick comments (3)
frontend/src/routes/_authenticated/chat.tsx (1)

2601-2605: Consider aligning with the shouldWireClarification pattern for robustness.

The condition message.externalId === "current-resp" works when currentResp.messageId is null, but if the messageId is assigned before streaming ends, this check fails while the message is still the active streaming response. The shouldWireClarification check at line 2509 handles this more robustly.

♻️ Optional: more robust check
 timeTakenMs={
-  message.externalId === "current-resp"
+  currentResp && message.externalId === (currentResp.messageId || "current-resp")
     ? streamTimeTakenMs
     : message.timeTakenMs ?? undefined
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/_authenticated/chat.tsx` around lines 2601 - 2605, The
timeTakenMs assignment should use the same robust check as
shouldWireClarification instead of comparing message.externalId ===
"current-resp"; update the ternary that sets timeTakenMs (currently switching
between streamTimeTakenMs and message.timeTakenMs) to use the
shouldWireClarification(...) result (the same call/site used at line 2509) so it
correctly picks streamTimeTakenMs while the response is still streaming even if
currentResp.messageId becomes non-null; ensure you reference the same
parameters/vars passed to shouldWireClarification when replacing the externalId
comparison.
frontend/src/components/ReasoningContext.tsx (2)

450-468: Expose disclosure state on the toggle buttons.

These controls show and hide content but never publish aria-expanded, which makes the new reasoning UI harder to understand in a screen reader. Adding aria-expanded here would close that gap with almost no code.

♿ Small accessibility improvement
         <button
           type="button"
           onClick={() => setExpanded((e) => !e)}
+          aria-expanded={expanded}
           className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
         >

Apply the same addition to the ToolBlock and AgentBlock toggle buttons. If you want to go one step further, pair it with aria-controls on the collapsible content container.

Also applies to: 591-605, 667-679

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ReasoningContext.tsx` around lines 450 - 468, The
toggle button in ReasoningContext.tsx is missing the accessible disclosure
state; update the button that calls setExpanded and uses the expanded variable
to include aria-expanded={expanded} (and optionally add aria-controls linking to
the collapsible container's id) so screen readers announce its state; apply the
same aria-expanded addition to the corresponding toggle buttons in ToolBlock and
AgentBlock (references: setExpanded, expanded, label/plan.goal,
completedCount/total, isStreaming/hasInProgress) to ensure consistent
accessibility.

496-505: Plan for React 19 JSX typing when upgrading @types/react.

The repo currently uses React 19.2.1 with @types/react 18.3.27. While the global JSX namespace still works with @types/react 18, upgrading to @types/react 19 (which should align with React 19) will remove this namespace and require React.JSX.Element instead.

Replace JSX.Element with React.JSX.Element in these signatures to prepare for the @types/react upgrade:

Locations to update
  • ReasoningStepComponent prop type (line 504)
  • ToolBlock prop type (line 583)
  • AgentBlock prop type (line 640)
  • ReasoningContextValue type (line 745)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ReasoningContext.tsx` around lines 496 - 505, The
prop/type declarations use the global JSX namespace which will break when
`@types/react` is upgraded; update all occurrences of JSX.Element to
React.JSX.Element (including optional null unions) — specifically change the
return/prop types in ReasoningStepComponent (prop getAppIcon), ToolBlock (its
getAppIcon prop), AgentBlock (its getAppIcon prop) and the ReasoningContextValue
type to use React.JSX.Element | null instead of JSX.Element | null so the code
remains compatible with `@types/react` 19.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/api/chat/message-agents.ts`:
- Around line 5008-5012: agentContext.runtime.emitReasoning is currently wired
directly to emitReasoningStep, so transient SSE write failures can throw and
abort agent runs; change the binding so emitReasoning is wrapped in a try/catch
that calls emitReasoningStep(payload as ReasoningEventPayload) and logs (but
does not rethrow) any errors from writeSSE, mirroring the error-swallowing
wrapper used elsewhere (e.g., emitReasoningEvent). Apply the same safe wrapper
when setting up the delegated-agent runtime (the delegated-agent runtime
assignment around lines where emitReasoning is wired) so delegated-agent
start/completion and synthesis start/completion calls won't fail the run on SSE
write failures.
- Around line 1786-1795: The dedup logic was partially migrated to
getFragmentDedupKey but elsewhere code still checks
gatheredFragmentsKeys.has(c.id), causing identical Vespa documents with new
fragment ids to bypass dedup; update places that use c.id for dedup (e.g., the
filter at the earlier check that calls gatheredFragmentsKeys.has(c.id)) to
compute the fragment key via getFragmentDedupKey(c) (and skip when it returns
falsy) and use gatheredFragmentsKeys.has(key) instead, ensuring
gatheredFragmentsKeys stores and checks the dedup keys consistently with the
loop that uses getFragmentDedupKey(fragment).

In `@server/api/chat/reasoning-steps.ts`:
- Around line 46-61: TOOL_DISPLAY is missing an entry for
XyneTools.MetadataRetrieval so that metadata retrieval uses the generic fallback
text; add a mapping for XyneTools.MetadataRetrieval to TOOL_DISPLAY (the same
object that contains entries like XyneTools.searchGlobal and
XyneTools.searchKnowledgeBase) with an appropriate executing and completed
string (e.g., "Retrieving metadata." and "Metadata retrieved.") so the module
uses the centralized copy instead of the generic fallback.

---

Outside diff comments:
In `@server/api/chat/message-agents.ts`:
- Around line 5364-5389: The first-turn expectations are being buffered instead
of persisted which causes turn 0 reviews to miss expected_results; update the
delegated-agent path to mirror the main-agent change so that whenever
extractedExpectations are collected you push them into pendingExpectations and
agentContext.currentTurnArtifacts.expectations and then call
recordExpectationsForTurn(currentTurn, extractedExpectations) when currentTurn
>= 0 (i.e., ensure turn 0 is recorded rather than only currentTurn > 0),
otherwise push to expectationBuffer only for genuinely future turns; apply this
same fix in the delegated-agent block referenced around the other occurrence
(the block around lines 6598-6609) so both paths use the same currentTurn
handling and calls to recordExpectationsForTurn, Logger.debug,
pendingExpectations, expectationBuffer, and
agentContext.currentTurnArtifacts.expectations.

---

Duplicate comments:
In `@frontend/src/components/ReasoningContext.tsx`:
- Around line 404-423: The fallback only checks steps[i].stage and misses staged
substeps in older turns, so update the fallback scanning logic (after
latestStage ??= lastTop.stage) to also inspect each steps[i].substeps for a
stage (mirroring the earlier loop over lastTop.substeps) before falling back to
steps[i].stage; specifically, in the for loop that iterates steps from the end,
check steps[i].substeps (if present) from last to first for a .stage and set
latestStage to that stage, otherwise fall back to steps[i].stage, ensuring
STAGE_PROGRESS_TEXT[latestStage] uses the most recent substep stage when
present.

In `@server/api/chat/message-agents.ts`:
- Around line 4062-4068: The current rebuild only adds expanded sheet ids from
history, dropping prior image attachments; update the aggregation that builds
allReferencedFileIds so it carries forward raw historical file ids
(historyFileIds) in addition to sheet expansions. Specifically, in the block
that computes allReferencedFileIds (which uses referencedFileIds,
historyFileIds, and expandSheetIds), include historyFileIds directly (not only
historyFileIds.flatMap(...)) so image file ids from
collectReferencedFileIdsUntilCompaction are preserved when reconstructing
initialAttachmentContext.

---

Nitpick comments:
In `@frontend/src/components/ReasoningContext.tsx`:
- Around line 450-468: The toggle button in ReasoningContext.tsx is missing the
accessible disclosure state; update the button that calls setExpanded and uses
the expanded variable to include aria-expanded={expanded} (and optionally add
aria-controls linking to the collapsible container's id) so screen readers
announce its state; apply the same aria-expanded addition to the corresponding
toggle buttons in ToolBlock and AgentBlock (references: setExpanded, expanded,
label/plan.goal, completedCount/total, isStreaming/hasInProgress) to ensure
consistent accessibility.
- Around line 496-505: The prop/type declarations use the global JSX namespace
which will break when `@types/react` is upgraded; update all occurrences of
JSX.Element to React.JSX.Element (including optional null unions) — specifically
change the return/prop types in ReasoningStepComponent (prop getAppIcon),
ToolBlock (its getAppIcon prop), AgentBlock (its getAppIcon prop) and the
ReasoningContextValue type to use React.JSX.Element | null instead of
JSX.Element | null so the code remains compatible with `@types/react` 19.

In `@frontend/src/routes/_authenticated/chat.tsx`:
- Around line 2601-2605: The timeTakenMs assignment should use the same robust
check as shouldWireClarification instead of comparing message.externalId ===
"current-resp"; update the ternary that sets timeTakenMs (currently switching
between streamTimeTakenMs and message.timeTakenMs) to use the
shouldWireClarification(...) result (the same call/site used at line 2509) so it
correctly picks streamTimeTakenMs while the response is still streaming even if
currentResp.messageId becomes non-null; ensure you reference the same
parameters/vars passed to shouldWireClarification when replacing the externalId
comparison.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 599f8623-6409-4a76-92de-a7b32cca2462

📥 Commits

Reviewing files that changed from the base of the PR and between 62649f3 and f78d9e0.

📒 Files selected for processing (5)
  • frontend/src/components/ReasoningContext.tsx
  • frontend/src/routes/_authenticated/chat.tsx
  • frontend/src/routes/_authenticated/index.tsx
  • server/api/chat/message-agents.ts
  • server/api/chat/reasoning-steps.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/routes/_authenticated/index.tsx

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
server/api/search.ts (1)

527-550: ⚠️ Potential issue | 🔴 Critical

Remove includeKnowledgeBaseInSearch property—not supported in VespaQueryConfig.

The property includeKnowledgeBaseInSearch does not exist in VespaQueryConfig from @xyne/vespa-ts and causes build failure TS2353 at lines 533 and 549.

To include/exclude the knowledge base, use excludedApps instead. When isSebi is true, omit Apps.KnowledgeBase from excludedApps (or leave it unset). When isSebi is false, add Apps.KnowledgeBase to excludedApps.

Remove both instances of includeKnowledgeBaseInSearch: isSebi and conditionally set excludedApps based on the isSebi flag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/search.ts` around lines 527 - 550, The code is passing an
unsupported includeKnowledgeBaseInSearch option to searchVespa
(VespaQueryConfig); remove both includeKnowledgeBaseInSearch: isSebi entries and
instead set excludedApps based on isSebi when calling searchVespa: if isSebi is
true leave excludedApps unset (or omit Apps.KnowledgeBase), otherwise include
Apps.KnowledgeBase in excludedApps; update both searchVespa call sites (the one
that runs with Promise.all and the else branch) to use excludedApps accordingly
while keeping other params (alpha, limit, requestDebug, offset, timestampRange,
rankProfile) and leaving updateUserQueryHistory, groupCount, results logic
unchanged.
server/api/chat/message-agents.ts (1)

3221-3270: ⚠️ Potential issue | 🟠 Major

Keep a terminal reasoning event for failed delegations.

agentDelegated(...) is emitted before executeCustomAgent(), but the error branch returns a plain ToolResponse.error(...). Later, afterToolExecutionHook() only emits agentCompleted(...) for successful runPublicAgent results and also skips the generic toolCompleted(...) path for this tool, so failed delegations never get a closing event and the delegated reasoning block can remain stuck in a running state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/message-agents.ts` around lines 3221 - 3270, The
failed-delegation branch returns ToolResponse.error without emitting a closing
reasoning event, leaving the delegated block open; before returning when
toolOutput.error is truthy (inside the run_public_agent flow where
delegationRunId and delegatedAgentName are set and executeCustomAgent was
called), call and await context.runtime?.emitReasoning with
ReasoningSteps.agentCompleted(...) (or an equivalent failure payload) enriched
with agent: delegatedAgentName, delegationRunId, parentAgent: "Main" and the
error details, then return the ToolResponse.error; use the same emitReasoning
pattern used in the success path (mirror the reasoningEmitter shape) so failed
delegations always emit a terminal agentCompleted event.
♻️ Duplicate comments (1)
server/api/chat/message-agents.ts (1)

4062-4068: ⚠️ Potential issue | 🟠 Major

Historical image and thread context is still dropped on follow-ups.

This only rehydrates historical non-image fileIds. Prior imageAttachmentFileIds and threadIds are still sourced exclusively from the current request, so follow-up questions about an earlier uploaded image or an earlier thread-linked email will still rebuild agentContext without that context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/chat/message-agents.ts` around lines 4062 - 4068, The code only
merges non-image fileIds from history (via
collectReferencedFileIdsUntilCompaction and expandSheetIds) so historical
imageAttachmentFileIds and threadIds are still omitted when rebuilding
agentContext; update the logic to also extract and merge historical
imageAttachmentFileIds and threadIds from previousConversationHistory (either by
extending collectReferencedFileIdsUntilCompaction to return those arrays or
adding small helpers like collectReferencedImageAttachmentIdsUntilCompaction and
collectReferencedThreadIdsUntilCompaction), then include those IDs when
constructing allReferencedFileIds and when populating agentContext (ensure
imageAttachmentFileIds and threadIds are combined with the current request’s
values).
🧹 Nitpick comments (5)
frontend/src/lib/common.tsx (1)

140-155: Consider consolidating duplicate icon rendering branches.

Lines 140-147 and 148-155 return identical JSX. These can be merged into a single condition:

♻️ Proposed consolidation
-  } else if (app === Apps.KnowledgeBase && entity === SystemEntity.SystemInfo) {
+  } else if (app === Apps.KnowledgeBase && (entity === SystemEntity.SystemInfo || entity === "file")) {
     return (
       <BookOpen
         size={size?.w || 12}
         className={`${classNameVal} dark:stroke-[`#F1F3F4`]`}
         stroke="#384049"
       />
     )
-  } else if (app === Apps.KnowledgeBase && entity === "file") {
-    return (
-      <BookOpen
-        size={size?.w || 12}
-        className={`${classNameVal} dark:stroke-[`#F1F3F4`]`}
-        stroke="#384049"
-      />
-    )
   } else if (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/common.tsx` around lines 140 - 155, Two consecutive branches
in the icon-rendering logic both check Apps.KnowledgeBase and return identical
BookOpen JSX (the conditions using SystemEntity.SystemInfo and entity ===
"file"); consolidate them by combining the conditions into a single branch
(e.g., check app === Apps.KnowledgeBase && (entity === SystemEntity.SystemInfo
|| entity === "file")) and keep the single BookOpen return to eliminate
duplicate JSX while preserving classNameVal, size, and stroke props.
frontend/src/components/SearchResult.tsx (1)

437-443: Type assertions in the kb_items branch indicate incomplete type narrowing.

The kb_items branch requires multiple inline type assertions ((result as { fileName?: string }), (result as { url?: string }), (result as { updatedAt?: number })), while other discriminated union variants (file, user, mail) access properties directly without casting. This pattern suggests the SearchResultDiscriminatedUnion type may not properly include the kb_items variant with its full property set. If the discriminated union is correctly defined, TypeScript should automatically narrow the type when result.type === "kb_items", eliminating the need for manual assertions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/SearchResult.tsx` around lines 437 - 443, The
kb_items branch is using inline type assertions (e.g., (result as { fileName?:
string }), (result as { url?: string }), (result as { updatedAt?: number }))
because TypeScript isn't narrowing result when result.type === "kb_items"; fix
the SearchResultDiscriminatedUnion so it includes a proper kb_items variant with
the expected properties (fileName, url, updatedAt, chunk/summary shape) or add a
named interface (e.g., KbItemResult) and include it in the union, then remove
the inline casts in the SearchResult.tsx branch (where title and chunkText are
derived) so result is directly typed and narrows correctly when result.type ===
"kb_items".
frontend/src/components/Autocomplete.tsx (1)

139-146: The title field access is correct; remove the invalid comparison with SearchResult.tsx.

KbFileAutocomplete has a title field per the schema definition (AutocompleteKbFileSchema at line 348 of server/shared/types.ts), so kbResult.title will render correctly. SearchResult.tsx uses a different discriminated union type (SearchResultDiscriminatedUnion from @xyne/vespa-ts) with different kb_items schema, making the field name comparison between the two components invalid.

The type assertion on line 140 is redundant since AutocompleteSchema is a properly defined discriminated union—TypeScript should automatically narrow the type after the result.type === "kb_items" check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Autocomplete.tsx` around lines 139 - 146, The code
incorrectly compares field names against SearchResult.tsx and uses a redundant
type assertion; inside Autocomplete.tsx where you check result.type ===
"kb_items" (and cast to KbFileAutocomplete), remove the invalid comparison to
SearchResult and the unnecessary "as KbFileAutocomplete" cast—rely on the
AutocompleteSchema discriminated union narrowing and directly use kbResult.title
(or result.title) to render the title, ensuring any import/usage of
KbFileAutocomplete matches the AutocompleteSchema definition.
server/vespa/schemas/ticket.sd (2)

613-623: Renormalize the title-boost branch before blending it with alpha.

Line 613 says the native-rank branch is “just to balance the vector score”, but vector_score is a closeness score in [0, 1] while nativeRank is a normalized text score. Multiplying the lexical side by 5 and 0.2 makes that branch several times larger than the vector branch, so alpha stops being an intuitive semantic-vs-lexical dial. If that isn’t intentional, normalize the weighted native-rank sum and reuse it in both phases. (docs.vespa.ai)

♻️ Suggested refactor
   rank-profile title_boosted_hybrid inherits initial {
-    `#nativeRank`(chunk) just to balance the vector score
+    function title_native_rank() {
+      expression: ((5 * nativeRank(subject)) + (0.2 * nativeRank(description))) / 5.2
+    }
+
     first-phase {
-      expression: (query(alpha) * vector_score) + ((1 - query(alpha)) * (5 * nativeRank(subject)+0.2*nativeRank(description)))
+      expression: (query(alpha) * vector_score) + ((1 - query(alpha)) * title_native_rank)
     }
     
     global-phase {
       expression {
         (
           (query(alpha) * vector_score) + 
-          ((1 - query(alpha)) * (5 * nativeRank(subject) + 0.2*nativeRank(description)))
+          ((1 - query(alpha)) * title_native_rank)
         ) * doc_recency
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/vespa/schemas/ticket.sd` around lines 613 - 623, The lexical branch
(nativeRank(subject)/nativeRank(description)) is scaled unequally to
vector_score making query(alpha) non-intuitive; compute a normalized
lexical_score by combining nativeRank(subject) and nativeRank(description) into
a single weighted sum, then renormalize it back into [0,1] (e.g., divide by the
sum of weights or apply min-max scaling) and replace the raw weighted expression
in both first-phase and global-phase with that normalized lexical_score so that
(query(alpha) * vector_score) + ((1 - query(alpha)) * lexical_score) yields a
balanced, consistent alpha blend (also multiply the whole blended expression by
doc_recency in global-phase as before).

612-615: Document the query tensor difference between rank profiles.

Line 612 inherits initial, which declares query(e) as the tensor input, while the existing hybrid profile (line 479) declares query(q). Although no current callers in the codebase reference either profile, ensure any future migration or adoption of title_boosted_hybrid uses the correct tensor name. If callers switch from hybrid to title_boosted_hybrid, they must update query inputs from query(q) to query(e), or add query(q) to the profile definition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/vespa/schemas/ticket.sd` around lines 612 - 615, The rank-profile
title_boosted_hybrid currently inherits initial which uses the tensor input name
query(e), while the existing hybrid profile uses query(q); update the
title_boosted_hybrid profile so its query tensor name matches expected callers —
either change its declaration to include query(q) in addition to or instead of
query(e), or document/rename it and update any callers to pass query(e) if you
intend to keep initial's naming; reference the rank-profile
title_boosted_hybrid, the inherited profile initial, and the hybrid profile
(which uses query(q)) when making the change so the tensor input names are
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/ChatBox.tsx`:
- Around line 3114-3116: The agent-mode controls are being hidden solely when
isAgenticByDefault is true, which can remove the toggle even if the effective
mode (isAgenticMode) is false; update the visibility logic in ChatBox.tsx
(around the blocks guarded by {!isAgenticByDefault && showAdvancedOptions} and
the similar block at the second location) to use an effective mode value
instead—either coerce effectiveIsAgentic = isAgenticByDefault || isAgenticMode
and hide controls only when effectiveIsAgentic is true, or ensure you set
effectiveIsAgentic to true when VITE_AGENTIC_BY_DEFAULT is set and only then
hide the Agent toggle/MCP picker; apply this change to both occurrences (the one
near the shown diff and the one at the other reported location).

In `@frontend/src/components/SearchResult.tsx`:
- Around line 447-455: The anchor in SearchResult.tsx uses (result as { url?:
string }).url which doesn't exist on KbFileResponseSchema causing broken links;
update rendering to build a valid href from existing fields instead of relying
on url — for example use a constructed path like `/kb/${result.docId}` or
`/kb/${result.docId}#${result.itemId}` (use result.docId and result.itemId when
present), and ensure the component still falls back to "#" only as a last
resort; adjust the type assertion/remove the bogus url cast and update any
usages of getIcon(result.app, result.entity, ...) to continue working with the
same result object.

In `@server/api/chat/message-agents.ts`:
- Around line 4487-4504: Assign mainRunIdRef via mainRunIdRef.current =
generateRunId() before any calls to emitReasoningStep so the initial events
(e.g., ReasoningSteps.attachmentAnalyzing and
ReasoningSteps.attachmentExtracted) carry a stable runId; update the code path
that prepares attachments (the block calling prepareInitialAttachmentContext and
then emitReasoningStep) to ensure mainRunIdRef is set first, and mirror the same
change for the other occurrence that emits reasoning steps later (the block
around the second emitReasoningStep calls). Ensure you reference mainRunIdRef,
generateRunId(), emitReasoningStep, and the ReasoningSteps.* helpers when making
the change so all early events use the generated runId.

In `@server/api/search.ts`:
- Line 295: The call sites pass an extra positional isSebi flag to functions
that don't support it (autocomplete and groupVespaSearch); fix by removing the
extra positional parameter and either upgrade `@xyne/vespa-ts` to a version that
accepts these options or implement wrapper functions (follow the searchVespa
pattern) named e.g. autocompleteWithSebi and groupVespaSearchWithSebi that
accept (query, email, ..., isSebi), handle the flag internally, build the proper
options object, and then call the underlying library's autocomplete and
groupVespaSearch methods with only the supported parameters; update the calls on
server/api/search.ts to use the new wrappers (or the upgraded library API) and
ensure no extra positional args are passed.

---

Outside diff comments:
In `@server/api/chat/message-agents.ts`:
- Around line 3221-3270: The failed-delegation branch returns ToolResponse.error
without emitting a closing reasoning event, leaving the delegated block open;
before returning when toolOutput.error is truthy (inside the run_public_agent
flow where delegationRunId and delegatedAgentName are set and executeCustomAgent
was called), call and await context.runtime?.emitReasoning with
ReasoningSteps.agentCompleted(...) (or an equivalent failure payload) enriched
with agent: delegatedAgentName, delegationRunId, parentAgent: "Main" and the
error details, then return the ToolResponse.error; use the same emitReasoning
pattern used in the success path (mirror the reasoningEmitter shape) so failed
delegations always emit a terminal agentCompleted event.

In `@server/api/search.ts`:
- Around line 527-550: The code is passing an unsupported
includeKnowledgeBaseInSearch option to searchVespa (VespaQueryConfig); remove
both includeKnowledgeBaseInSearch: isSebi entries and instead set excludedApps
based on isSebi when calling searchVespa: if isSebi is true leave excludedApps
unset (or omit Apps.KnowledgeBase), otherwise include Apps.KnowledgeBase in
excludedApps; update both searchVespa call sites (the one that runs with
Promise.all and the else branch) to use excludedApps accordingly while keeping
other params (alpha, limit, requestDebug, offset, timestampRange, rankProfile)
and leaving updateUserQueryHistory, groupCount, results logic unchanged.

---

Duplicate comments:
In `@server/api/chat/message-agents.ts`:
- Around line 4062-4068: The code only merges non-image fileIds from history
(via collectReferencedFileIdsUntilCompaction and expandSheetIds) so historical
imageAttachmentFileIds and threadIds are still omitted when rebuilding
agentContext; update the logic to also extract and merge historical
imageAttachmentFileIds and threadIds from previousConversationHistory (either by
extending collectReferencedFileIdsUntilCompaction to return those arrays or
adding small helpers like collectReferencedImageAttachmentIdsUntilCompaction and
collectReferencedThreadIdsUntilCompaction), then include those IDs when
constructing allReferencedFileIds and when populating agentContext (ensure
imageAttachmentFileIds and threadIds are combined with the current request’s
values).

---

Nitpick comments:
In `@frontend/src/components/Autocomplete.tsx`:
- Around line 139-146: The code incorrectly compares field names against
SearchResult.tsx and uses a redundant type assertion; inside Autocomplete.tsx
where you check result.type === "kb_items" (and cast to KbFileAutocomplete),
remove the invalid comparison to SearchResult and the unnecessary "as
KbFileAutocomplete" cast—rely on the AutocompleteSchema discriminated union
narrowing and directly use kbResult.title (or result.title) to render the title,
ensuring any import/usage of KbFileAutocomplete matches the AutocompleteSchema
definition.

In `@frontend/src/components/SearchResult.tsx`:
- Around line 437-443: The kb_items branch is using inline type assertions
(e.g., (result as { fileName?: string }), (result as { url?: string }), (result
as { updatedAt?: number })) because TypeScript isn't narrowing result when
result.type === "kb_items"; fix the SearchResultDiscriminatedUnion so it
includes a proper kb_items variant with the expected properties (fileName, url,
updatedAt, chunk/summary shape) or add a named interface (e.g., KbItemResult)
and include it in the union, then remove the inline casts in the
SearchResult.tsx branch (where title and chunkText are derived) so result is
directly typed and narrows correctly when result.type === "kb_items".

In `@frontend/src/lib/common.tsx`:
- Around line 140-155: Two consecutive branches in the icon-rendering logic both
check Apps.KnowledgeBase and return identical BookOpen JSX (the conditions using
SystemEntity.SystemInfo and entity === "file"); consolidate them by combining
the conditions into a single branch (e.g., check app === Apps.KnowledgeBase &&
(entity === SystemEntity.SystemInfo || entity === "file")) and keep the single
BookOpen return to eliminate duplicate JSX while preserving classNameVal, size,
and stroke props.

In `@server/vespa/schemas/ticket.sd`:
- Around line 613-623: The lexical branch
(nativeRank(subject)/nativeRank(description)) is scaled unequally to
vector_score making query(alpha) non-intuitive; compute a normalized
lexical_score by combining nativeRank(subject) and nativeRank(description) into
a single weighted sum, then renormalize it back into [0,1] (e.g., divide by the
sum of weights or apply min-max scaling) and replace the raw weighted expression
in both first-phase and global-phase with that normalized lexical_score so that
(query(alpha) * vector_score) + ((1 - query(alpha)) * lexical_score) yields a
balanced, consistent alpha blend (also multiply the whole blended expression by
doc_recency in global-phase as before).
- Around line 612-615: The rank-profile title_boosted_hybrid currently inherits
initial which uses the tensor input name query(e), while the existing hybrid
profile uses query(q); update the title_boosted_hybrid profile so its query
tensor name matches expected callers — either change its declaration to include
query(q) in addition to or instead of query(e), or document/rename it and update
any callers to pass query(e) if you intend to keep initial's naming; reference
the rank-profile title_boosted_hybrid, the inherited profile initial, and the
hybrid profile (which uses query(q)) when making the change so the tensor input
names are consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31452d34-6211-4634-9554-3ec0e0898c9d

📥 Commits

Reviewing files that changed from the base of the PR and between f78d9e0 and 0deff65.

📒 Files selected for processing (9)
  • frontend/src/components/Autocomplete.tsx
  • frontend/src/components/ChatBox.tsx
  • frontend/src/components/SearchResult.tsx
  • frontend/src/lib/common.tsx
  • server/api/chat/message-agents.ts
  • server/api/search.ts
  • server/config.ts
  • server/shared/types.ts
  • server/vespa/schemas/ticket.sd

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
frontend/src/components/ChatBox.tsx (1)

367-368: ⚠️ Potential issue | 🟠 Major

Keep an escape hatch when agentic-default is enabled.

isAgenticByDefault only provides the default prop value here; parents can still pass false. In that state these guards hide both the MCP picker and the Agent toggle, so the user can get stuck in non-agent mode with no way to turn it back on. Use an effective mode (isAgenticByDefault || isAgenticMode) for both rendering and interactivity.

Also applies to: 385-385, 3145-3147, 3750-3751

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ChatBox.tsx` around lines 367 - 368, The guards use
isAgenticByDefault as a prop default which can hide both the MCP picker and the
Agent toggle and leave the UI stuck; change rendering and interaction checks to
use an effective mode (e.g., const effectiveAgentic = isAgenticByDefault ||
isAgenticMode) so both visibility and interactivity for the MCP picker and Agent
toggle use effectiveAgentic instead of raw isAgenticByDefault; update all
occurrences referenced (around symbols isAgenticByDefault, isAgenticMode and the
MCP/Agent toggle render blocks noted at the duplicated locations) so parents can
still flip agentic mode at runtime.
🧹 Nitpick comments (1)
server/api/search.ts (1)

610-612: Consider passing email to VespaSearchResponseToSearchResult for consistency.

The main SearchApi (line 567-571) and agent search (line 453-458) pass email as the third argument, but this call omits it. While SearchSlackChannels (line 637-639) also omits it, verify whether KB search results require user-specific transformations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/search.ts` around lines 610 - 612, The call to
VespaSearchResponseToSearchResult here omits the user email argument used
elsewhere; update the call in the SearchApi flow to pass the current user's
email as the third parameter (matching the other calls that pass email) so
user-specific transformations are applied consistently, and audit the similar
call in SearchSlackChannels (which also omits email) to decide whether it
likewise should pass the email or intentionally omit it; modify
VespaSearchResponseToSearchResult invocations (and callers passing
chunkDocument) to include the email when user-specific behavior is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/ChatBox.tsx`:
- Around line 1565-1588: The totalCount calculation currently sums backend
counts (fetchedTotalCount = (data.count ?? 0) + (kbData.count ?? 0)) causing
"Load More" to remain when items are duplicated; change this to use the deduped
union used for globalResults by deriving the total from the dedup map/array
instead (e.g., use byDocId.size or results.length) and call setTotalCount with
that value so totalCount reflects unique results after the docId deduplication
used by mainResults/kbResults/byDocId.

In `@frontend/src/routes/_authenticated/search.tsx`:
- Around line 423-426: When appending new paginated results in the search
handler, deduplicate items in merged before calling setResults so documents with
the same docId from /search and /search/knowledge-base aren't rendered twice;
specifically, in the branch that checks newOffset > 0, compute a filteredMerged
by removing any item whose docId already exists in prevResults (use
setResults(prev => { const existingIds = new Set(prev.map(r => r.docId)); return
[...prev, ...merged.filter(m => !existingIds.has(m.docId))]; })) and then call
setResults with that filtered array, while keeping the else branch to replace
results with merged unchanged.
- Around line 455-468: The sidebar total is using the raw endpoint counts
(data.count + kbData.count) when sumGroupCounts(mergedGroups) is zero, which
overcounts duplicate docs; instead compute a deduped total from the merged
result set (dedupe by docId) and setSearchMeta({ totalCount: dedupedTotal }) —
i.e., after mergeGroupCounts(…) and wherever the search results are merged,
produce a unique-docId count (call it dedupedTotal or uniqueCount) and use that
as the fallback/current total instead of (data.count ?? 0) + (kbData.count ??
0); update references to mergeGroupCounts, sumGroupCounts, mergedGroups and
setSearchMeta to use this deduped total.

In `@server/api/search.ts`:
- Around line 598-608: searchVespaKnowledgeBase is being called without the
computed timestampRange, causing search results to ignore the same time filter
used by groupVespaSearchKnowledgeBase; update the call to
searchVespaKnowledgeBase(decodedQuery, email, { limit: page, offset,
rankProfile: SearchModes.NativeRank, timestampRange }) so the options object
includes timestampRange (the value computed from lastUpdated) to ensure both
searchVespaKnowledgeBase and groupVespaSearchKnowledgeBase apply the same time
filtering.

In `@server/search/vespa.ts`:
- Around line 158-161: The code exports two methods that don't exist on
VespaService (searchVespaKnowledgeBase and groupVespaSearchKnowledgeBase),
causing module load failures; either upgrade the `@xyne/vespa-ts` library to a
version that exposes those methods or remove the broken exports and update
callsites. If upgrading is not possible, delete the exports
searchVespaKnowledgeBase and groupVespaSearchKnowledgeBase from the vespa.ts
module and change the usages in server/api/search.ts to call the existing
methods (e.g., searchVespa or groupVespaSearch) or add a runtime guard that
falls back to the non-KB methods; ensure all references to those two symbols are
removed or redirected so module initialization no longer tries to bind missing
methods.

---

Duplicate comments:
In `@frontend/src/components/ChatBox.tsx`:
- Around line 367-368: The guards use isAgenticByDefault as a prop default which
can hide both the MCP picker and the Agent toggle and leave the UI stuck; change
rendering and interaction checks to use an effective mode (e.g., const
effectiveAgentic = isAgenticByDefault || isAgenticMode) so both visibility and
interactivity for the MCP picker and Agent toggle use effectiveAgentic instead
of raw isAgenticByDefault; update all occurrences referenced (around symbols
isAgenticByDefault, isAgenticMode and the MCP/Agent toggle render blocks noted
at the duplicated locations) so parents can still flip agentic mode at runtime.

---

Nitpick comments:
In `@server/api/search.ts`:
- Around line 610-612: The call to VespaSearchResponseToSearchResult here omits
the user email argument used elsewhere; update the call in the SearchApi flow to
pass the current user's email as the third parameter (matching the other calls
that pass email) so user-specific transformations are applied consistently, and
audit the similar call in SearchSlackChannels (which also omits email) to decide
whether it likewise should pass the email or intentionally omit it; modify
VespaSearchResponseToSearchResult invocations (and callers passing
chunkDocument) to include the email when user-specific behavior is required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b99b3694-2006-424a-a16b-b48e2e3be4f3

📥 Commits

Reviewing files that changed from the base of the PR and between 0deff65 and bd8e5c3.

📒 Files selected for processing (5)
  • frontend/src/components/ChatBox.tsx
  • frontend/src/routes/_authenticated/search.tsx
  • server/api/search.ts
  • server/search/vespa.ts
  • server/server.ts

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.

2 participants