fix: target specific chat in MarkStale instead of broadcasting to all workspace chats (#23883)

## Problem

Subagent chats were receiving git context (branch, remote origin, PR
status) from their parent or sibling chats' git operations. When a git
operation triggers external auth, the workspace agent sends `chat_id`
identifying which chat initiated it — but this was broken at two levels:

1. **Agent side:** `CODER_CHAT_ID` was never injected into process
   environments. `chatd` sets `Coder-Chat-Id` HTTP headers and the
   agent extracts them for process isolation, but never propagated
   `CODER_CHAT_ID` to `cmd.Env`. So `gitaskpass` always sent an empty
   `chat_id`.

2. **Server side:** `workspaceAgentsExternalAuth` ignored the `chat_id`
   query param. `MarkStale` broadcast git context to **all** chats on
   the workspace via `filterChatsByWorkspaceID`.

## Fix

- Inject `CODER_CHAT_ID` into `cmd.Env` in `agentproc` when the chat
  ID is known, so `gitaskpass` can read and forward it.
- Read `chat_id` from query params in `workspaceAgentsExternalAuth`
  and thread it through `chatGitRef`.
- Refactor `MarkStale` to accept a `MarkStaleParams` struct. When
  `ChatID` is provided, target only that specific chat. When empty
  (legacy agents, non-chat git operations), fall back to the existing
  workspace-wide broadcast.
- Extract `markStaleSingle` helper to deduplicate the upsert+publish
  logic.

<details><summary>Investigation notes</summary>

### Data flow before fix

```
chatd → sets Coder-Chat-Id header on agent conn
agent → extracts chatID, stores on process struct
agent → does NOT set CODER_CHAT_ID in cmd.Env  ← gap 1
gitaskpass → reads CODER_CHAT_ID (always empty), sends chat_id=""
server handler → ignores chat_id query param     ← gap 2
MarkStale → broadcasts to ALL workspace chats
```

### Data flow after fix

```
chatd → sets Coder-Chat-Id header on agent conn
agent → extracts chatID, stores on process struct
agent → sets CODER_CHAT_ID in cmd.Env
gitaskpass → reads CODER_CHAT_ID, sends chat_id=<uuid>
server handler → reads chat_id, passes to MarkStale
MarkStale → targets only that specific chat
```

</details>
This commit is contained in:
Kyle Carberry
2026-04-01 13:04:59 +00:00
committed by GitHub
parent 2ea89e1f1b
commit 19e44f4136
5 changed files with 240 additions and 44 deletions
+20 -2
View File
@@ -42,6 +42,7 @@ import (
"github.com/coder/coder/v2/coderd/telemetry"
maputil "github.com/coder/coder/v2/coderd/util/maps"
"github.com/coder/coder/v2/coderd/wspubsub"
"github.com/coder/coder/v2/coderd/x/gitsync"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -1840,6 +1841,11 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
Branch: strings.TrimSpace(query.Get("git_branch")),
RemoteOrigin: strings.TrimSpace(query.Get("git_remote_origin")),
}
if raw := strings.TrimSpace(query.Get("chat_id")); raw != "" {
if parsed, err := uuid.Parse(raw); err == nil {
gitRef.ChatID = parsed
}
}
// Either match or configID must be provided!
match := query.Get("match")
if match == "" {
@@ -1938,7 +1944,13 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
// context is retained even if the flow requires an out-of-band login.
if gitRef.Branch != "" && gitRef.RemoteOrigin != "" {
//nolint:gocritic // Chat processor context required for cross-user chat lookup
api.gitSyncWorker.MarkStale(dbauthz.AsChatd(ctx), workspace.ID, workspace.OwnerID, gitRef.Branch, gitRef.RemoteOrigin)
api.gitSyncWorker.MarkStale(dbauthz.AsChatd(ctx), gitsync.MarkStaleParams{
WorkspaceID: workspace.ID,
OwnerID: workspace.OwnerID,
Branch: gitRef.Branch,
Origin: gitRef.RemoteOrigin,
ChatID: gitRef.ChatID,
})
}
var previousToken *database.ExternalAuthLink
@@ -2087,7 +2099,13 @@ func (api *API) workspaceAgentsExternalAuthListen(ctx context.Context, rw http.R
}
// MarkStale will trigger a refresh by coderd/gitsync.
//nolint:gocritic // Chat processor context required for cross-user chat lookup
api.gitSyncWorker.MarkStale(dbauthz.AsChatd(ctx), workspace.ID, workspace.OwnerID, gitRef.Branch, gitRef.RemoteOrigin)
api.gitSyncWorker.MarkStale(dbauthz.AsChatd(ctx), gitsync.MarkStaleParams{
WorkspaceID: workspace.ID,
OwnerID: workspace.OwnerID,
Branch: gitRef.Branch,
Origin: gitRef.RemoteOrigin,
ChatID: gitRef.ChatID,
})
httpapi.Write(ctx, rw, http.StatusOK, resp)
return
}