Skip to content

fix: target the backend-selected chat agent for desktop, git, and terminal#26959

Draft
DanielleMaywood wants to merge 4 commits into
mainfrom
danielle/codagt-387-chat-desktop-picks-incorrect-agent-with-devcontainer
Draft

fix: target the backend-selected chat agent for desktop, git, and terminal#26959
DanielleMaywood wants to merge 4 commits into
mainfrom
danielle/codagt-387-chat-desktop-picks-incorrect-agent-with-devcontainer

Conversation

@DanielleMaywood

@DanielleMaywood DanielleMaywood commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

On templates with a dev container, Coder Agents' Portable Desktop and web terminal could nondeterministically target the dev container sub-agent instead of the workspace's root agent, making them unusable.

Problem

GetWorkspaceAgentsInLatestBuildByWorkspaceID has no ORDER BY and includes dev container sub-agents (rows with parent_id set), so its row order is unspecified. The bug surfaced in two places:

  • watchChatDesktop and watchChatGit in coderd/exp_chats.go used agents[0] from that query for both the agent conversion and the AgentConn dial, so the desktop and git streams could dial a sub-agent, which has no desktop.
  • AgentChatPage resolved its agent via getWorkspaceAgent(workspace, undefined), whose fallback returned the first agent across all resources, so the embedded web terminal could target a sub-agent.

Fix

  • The stream handlers now use agentselect.FindChatAgent, the same deterministic selector chatd uses (root agents only; display order, name, ID sort; -coderd-chat suffix preference). The package moved from coderd/x/chatd/internal/ to coderd/x/chatd/agentselect/ so coderd can import it.
  • The frontend never selects an agent itself. AgentChatPage uses the backend-selected agent from the chat's agent_id, and getWorkspaceAgent resolves strictly by ID with no first-agent fallback; when agent_id is missing, no agent is shown rather than an arbitrary one.
  • Because chatd only persists agent_id once a turn dials the workspace, getChat and listChats enrich responses with a best-effort agent_id (resolved via FindChatAgent, response-only, never persisted) when a chat has a bound workspace but no binding yet.

Testing

  • Mock-based handler test asserting the root agent is dialed when the database returns a sub-agent first.
  • dbmock test for the enrichment helper: root-over-sub selection, lookup dedup across root and child chats, null on error, no lookup when already bound or unbound.
  • Updated chatHelpers vitest cases for strict-by-ID resolution.
Root cause analysis and decision log

Root cause

  • GetWorkspaceAgentsInLatestBuildByWorkspaceID (coderd/database/queries/workspaceagents.sql) has no ORDER BY and no parent_id IS NULL filter, so non-deleted dev container sub-agents are included and row order is unspecified.
  • watchChatDesktop and watchChatGit (coderd/exp_chats.go) used agents[0] for both the db2sdk.WorkspaceAgent conversion and the AgentConn dial. With a dev container present, agents[0] could be the sub-agent.
  • The web terminal issue was frontend-side: AgentChatPage called getWorkspaceAgent(workspace, undefined), whose fallback returned the first agent across all resources.

Decisions

  • Reused the existing agentselect.FindChatAgent selector already used by coderd/x/chatd, rather than writing a new one. Moved it out of internal/ via git mv; the package API is unchanged.
  • On selection failure the stream handlers return 400 with a detail message, matching the surrounding error style for the empty-agents case.
  • The frontend does not replicate the selection logic. An earlier iteration mirrored FindChatAgent in TypeScript (including exposing display_order on codersdk.WorkspaceAgent); that was rejected in favor of the backend providing the selected agent, since the chat API already persists agent_id. The mirror and the display_order exposure were fully reverted; the net diff contains neither.
  • Enrichment in getChat/listChats is best-effort and response-only: on any error the field stays null and the UI shows no agent rather than an arbitrary one. Lookups are deduplicated per workspace within a request.
  • getWorkspaceAgent in chatHelpers lost its first-agent fallback; its only callers are in AgentChatPage, all of which now pass the chat's agent_id.

Verification

  • go build ./..., go test ./coderd/x/chatd/..., targeted TestWatchChatGit / TestWatchChatDesktop / TestEnrichChatAgentIDs runs, golangci-lint on changed packages
  • tsc -b, biome, vitest for chatHelpers.test.ts, Storybook tests for AgentChatPage (19 passed)
  • make pre-commit passed on every commit

This PR was generated by Coder Agents on behalf of @DanielleMaywood.

…t, and task terminal

The chat desktop and git watch handlers picked agents[0] from
GetWorkspaceAgentsInLatestBuildByWorkspaceID, which has no ORDER BY and
includes dev container sub-agents, so Portable Desktop could
nondeterministically target a sub-agent. The task page terminal had the
same problem via allApps.at(0)?.agent on the frontend.

Move chatd's agentselect package out of internal/ and use
FindChatAgent in watchChatDesktop and watchChatGit. On the frontend,
add getFirstRootAgent and use it for the task terminal target and the
terminal page's no-agent-name fallback, skipping agents with parent_id
set.
@linear-code

linear-code Bot commented Jul 2, 2026

Copy link
Copy Markdown

CODAGT-387

Replace getFirstRootAgent with findChatAgent, a strict TypeScript mirror
of agentselect.FindChatAgent, so the task terminal targets the same
agent as the chat desktop and git streams: root agents only, sorted by
display order, case-insensitive name, name, then ID, with the
-coderd-chat suffix preference. Error cases map to undefined.

Expose display_order on codersdk.WorkspaceAgent so the frontend can
apply the primary sort key, and cross-reference the two implementations
in doc comments so they stay in sync.
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/reference/api/agents.md

…ring selection

Replace the frontend TypeScript mirror of agentselect.FindChatAgent
with backend-provided data. getChat and listChats now enrich the
response with a best-effort agent_id (resolved via FindChatAgent) when
a chat has a bound workspace but chatd has not yet persisted the
binding, and AgentChatPage uses chat.agent_id instead of falling back
to the first workspace agent.

Revert the display_order exposure on codersdk.WorkspaceAgent and the
TaskPage changes; both existed only to support the frontend mirror.
getWorkspaceAgent in chatHelpers now resolves strictly by ID with no
first-agent fallback.
@DanielleMaywood DanielleMaywood changed the title fix: select root agent deterministically for chat desktop, git, and task terminal fix: target the backend-selected chat agent for desktop, git, and terminal Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant