Skip to content

fix: persist agent and query-refusal chats to thread history via API#5080

Open
angelplusultra wants to merge 5 commits intomasterfrom
5060-bug-agent-interactions-agent-are-not-persisted-to-thread-history-via-api
Open

fix: persist agent and query-refusal chats to thread history via API#5080
angelplusultra wants to merge 5 commits intomasterfrom
5060-bug-agent-interactions-agent-are-not-persisted-to-thread-history-via-api

Conversation

@angelplusultra
Copy link
Contributor

@angelplusultra angelplusultra commented Feb 27, 2026

Pull Request Type

  • ✨ feat (New feature)
  • 🐛 fix (Bug fix)
  • ♻️ refactor (Code refactoring without changing behavior)
  • 💄 style (UI style changes)
  • 🔨 chore (Build, CI, maintenance)
  • 📝 docs (Documentation updates)

Relevant Issues

resolves #5060

Description

API chats via chatSync and streamChat had inconsistent behavior around UI visibility, thread association, and conversation history. This PR unifies both handlers so all code paths behave identically.

Root causes:

  1. chatSync agent path was missing threadId and user, so agent chats were saved as orphan records with no thread association
  2. include was hardcoded inconsistently — some paths used true, some false, some relied on the default (true). There was no unified logic
  3. recentChatHistory always filtered include: true, which meant any chat saved with include: false (ephemeral session chats) was invisible to the LLM on subsequent calls — breaking conversation continuity for sessionId-based API usage

Changes:

  1. apiChatHandler.js — Both chatSync and streamChat now derive shouldIncludeInUI = !sessionId at the top. Every WorkspaceChats.new call uses this value, and consistently passes threadId, user, and apiSessionId.

  2. chats/index.jsrecentChatHistory no longer filters on include: true when apiSessionId is present, so ephemeral session chats are still available as LLM context.

Expected behavior with these changes:

UI Visibility

Condition include Visible in UI?
sessionId provided false No — ephemeral session, hidden from UI
userId provided, no sessionId true Yes — visible to that user
Neither provided (single-user mode) true Yes — no user scoping needed
Neither provided (multi-user mode) true Effectively no — user_id is null so no user's frontend query matches it

LLM Conversation History

Condition LLM receives prior messages? Scoped by
sessionId provided Yes api_session_id match
userId provided, no sessionId Yes user_id + thread_id + include: true
Neither provided Yes (single-user) / No (multi-user, messages effectively vanish) thread_id + include: true

Consistency across handlers

Code path chatSync streamChat
Agent shouldIncludeInUI, threadId ✅, user shouldIncludeInUI, threadId ✅, user
Query refusal (no embeddings) shouldIncludeInUI, threadId ✅, user shouldIncludeInUI, threadId ✅, user
Query refusal (no context) shouldIncludeInUI, threadId ✅, user shouldIncludeInUI, threadId ✅, user
Regular chat shouldIncludeInUI, threadId ✅, user shouldIncludeInUI, threadId ✅, user

Additional Information

  • The frontend websocket chat handler (stream.js) is unaffected — it does not go through apiChatHandler.js
  • Workspace-level API endpoints (/v1/workspace/:slug/chat) do not currently accept userId in the request body — user is always null. Only thread-level endpoints support userId.

Developer Validations

  • I ran yarn lint from the root of the repo & committed changes
  • Relevant documentation has been updated (if applicable)
  • I have tested my code functionality
  • Docker build succeeds locally

WorkspaceChat.new() was missing required fields in order to save the
prompt/response correctly. Added `threadId` and `include: true`
Copy link
Collaborator

@shatfield4 shatfield4 left a comment

Choose a reason for hiding this comment

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

Intended behavior here to not clutter the UI with API calls would be:

  • make all API calls include: false to prevent them from showing in the UI
  • Have developers using the admin API persist context by using the apiSessionId

There is a bug here in the apiChatHandler.js where chatSync and streamChat do not have the same behavior so be sure to resolve this so both functions set include: false always.

It looks like the main bug in here is that in recentChatHistory util function the query we do to get the context adds include: true which always returns nothing because we are filtering based on apiSessionId and include: true.

We need to change this so that when apiSessionId is provided to the recentChatHistory util, we remove the include: true from the query.

Copy link
Collaborator

@shatfield4 shatfield4 left a comment

Choose a reason for hiding this comment

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

Code looks good here and I tested it thoroughly. A few things I found:

  • We need to reject/throw and error if userId and sessionId are in the same request (as discussed with Tim over slack) since there is no real use case for this
  • Noticed that sessionId is silently ignored in both chatSync and streamChat thread endpoints

Copy link
Collaborator

@shatfield4 shatfield4 left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@timothycarambat timothycarambat left a comment

Choose a reason for hiding this comment

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

Given the complexity and confusion around this issue in Slack, it would be the right move to build a test suite for this so that it is code-clear as to why things work the way they do and to also prevent regressions since the code-paths/logic for history loading via the API is confusing and sparsely documented.

If we have tests, we know exactly how the API chat system should work in all types of conditions and params.

messageLimit = 20,
apiSessionId = null,
}) {
const where = {
Copy link
Member

Choose a reason for hiding this comment

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

This is a bad name, should just be something like conditions or something more clear imo

// API chats using a sessionId are saved with include: false to keep them
// hidden from the UI. When fetching history for an API session, we omit
// the include filter so prior chats are still available for LLM context.
if (!apiSessionId) where.include = true;
Copy link
Member

Choose a reason for hiding this comment

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

Include include: true to keep compatibility with the existing code prior just to be sure we are covering cases we might not be aware of.

then if apiSessionId is not null we can do include: false.


Actually, to clear something up:

  • If a chat is sent with apiSessionId we hide it from the UI with include: false

  • If a chat is sent with same session id, even though it is false, we should pull the historical chats anyway so that chat history exists

  • What about people using sessionIDs right now where history is still true, but then upgrade. Their history is now reset for the same sessionID, correct?

In the above case, if that is correct, shouldnt we just omit/delete include totally from the query?

Comment on lines +426 to +437
if (userId && sessionId) {
response.status(400).json({
id: uuidv4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error:
"Cannot pass both userId and sessionId. Use userId to chat as a user or sessionId for ephemeral sessions.",
});
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

Make this a locally defined middleware check to the endpoint here to prevent code duplication since it is the same thing for both endpoints

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Agent interactions (@agent) are not persisted to thread history via API

3 participants