Summary
Split out of #2814 , this PR is only the chat-based (in-app agent) half of that PR, plus two capabilities #2814 never had: the agent can now find, read, and actually switch the user into its own past chat threads. The popover UI search from #2814 (History/Recent popover text search) is deliberately left out; it'll ship separately if we still want it.
Three tools, meant to chain:
search-chat-history , search the user's own past chat threads by title AND message content , not title-only, since a thread's title can say nothing about what's actually inside it (e.g. a CSV account upload buried in a message). Returns a threadId + snippet per match.
load-chat-history , given a threadId, reads that thread's own message history (including prior tool calls/results) back into the agent's context for the current turn, so it can continue the topic without the user re-explaining it. Does not touch the frontend.
switch-chat-thread , given a threadId, actually moves the user's screen into that thread , the same effect as clicking it in the History/Recent popover.
All three registered on chatAgent / chatAgentUnderstudy (mastra/agents/chat-agent.ts) and the family-surface Master agent via IN_APP_ORCHESTRATION_FAMILIES (mastra/tools/master-surface.ts).
Implementation
services/chat/search-chat-threads.ts , shared search core behind search-chat-history. ILIKE match on chat_threads.name and chat_messages.content.
mastra/tools/chat/searchChatHistoryTool.ts / loadChatHistoryTool.ts / switchChatThreadTool.ts , the three tools. All self-scoped: verify the thread belongs to the calling user before doing anything, same "no such thread" message whether it's missing or owned by someone else (never confirms cross-user existence).
load-chat-history reuses loadHeadlessHistory/formatHeadlessHistory (mastra/utils/headless-history.ts) , the same loader that hydrates Slack/iMessage headless threads , rather than writing a new reader.
switch-chat-thread emits a thread-switch SSE object event (same mechanism manage-context's context-updated event already uses) carrying { threadId, name }.
apps/mail/.../responseProcessors/threadSwitchResponseProcessor.ts , the frontend side. Calls the store's existing switchThread(threadId, name) action (messagesSlice.ts) , the exact function the History/Recent popover click path already uses, which hydrates messages from the DB itself when they aren't cached. No new frontend loading logic , this only adds the agent-triggered entry point into an existing, already-tested mechanism.
- Switching is safe mid-generation:
agentConnectionSlice tracks streaming per threadId, and every response processor is bound to the thread it originated from regardless of which thread is active in the UI , so the agent's own in-flight response keeps streaming into its original thread; the user just isn't looking at it live until they switch back. That's an existing, tested state (background streaming into an inactive thread), not a new race.
db/migrations/chat_search_trgm.sql , pg_trgm GIN trigram indexes backing the ILIKE search. Must be applied out-of-band (CREATE INDEX CONCURRENTLY can't run in a transaction) , see the migration file's header for the exact psql invocation and pooler caveat.
Left out of this PR (still in #2814, not touched here): EmbeddedCedarChat.tsx (History popover text search UI), LeftSidebarContent.tsx (Recent popover text search UI), and the chat.searchThreads tRPC query those two UI surfaces call.
Known gaps carried over from #2814's review
- The Agent SDK harness (used by viewer accounts and some flagged users) doesn't have any of the three tools registered , only the Mastra chat agents and the family-surface Master agent do.
- The snippet query in
search-chat-threads.ts loads every matching message across a user's matched threads before picking one per thread in application code , fine at today'