refactor: remove legacy live-read and injected-history chat context paths (#26585)

This PR makes the agent-pushed pinned snapshot
(`chat_context_resources`) the sole source of workspace context for
chats, completing the "Release 5" cleanup. It removes legacy mechanisms
now superseded by the snapshot that agents push over dRPC
(`PushContextState`) and refresh via `chat-context/refresh`.

Removed:

- **Live-read at turn time.** MCP tool discovery, skill live-body reads,
and the instruction/skill history fallback that dialed the workspace on
every turn.
- **Context injected as message history.** The
`persist_workspace_context` generation action and its decision-loop
guard.
- **The legacy write path.** `POST`/`DELETE
/api/v2/workspaceagents/me/experimental/chat-context`, the agentsdk
`AddChatContext`/`ClearChatContext` methods, and the CLI one-shot
writer.
- **The `chats.last_injected_context` column** and all of its plumbing
(migration `000529`, queries, `db2sdk`, `dbauthz`, audit table, and the
frontend `ContextUsageIndicator` fallback).

Subagent context inheritance no longer copies parent context messages;
children now hydrate the parent's pinned `chat_context_resources` on
create, which yields an identical pin for the same workspace and agent.

What stays (still served by the live agent connection, not the
snapshot): `read_skill_file` supporting-file reads, `read_skill`
supporting-file listing, and MCP tool execution.

> [!NOTE]
> Migration `000529` drops `chats.last_injected_context` and recreates
the `chats_expanded` view without it. The down migration restores both.

<details>
<summary>Decision log (D1-D5)</summary>

- **D1 (subagent inheritance):** Re-point inheritance from the legacy
message copy to a pinned hydrate. Children call
`hydrateChatContextOnCreate` instead of copying parent context messages.
- **D2 (`persist_workspace_context`):** Remove the generation action
entirely along with the decision-loop guard it existed to satisfy, since
context is never injected into history anymore.
- **D3 (legacy HTTP + CLI):** Remove the experimental `chat-context`
POST/DELETE endpoints, the agentsdk methods, and the CLI one-shot. The
dRPC push + `chat-context/refresh` replace them.
- **D4 (frontend fallback):** Remove the `last_injected_context`
fallback in `ContextUsageIndicator`; pinned `resources` are the sole
source.
- **D5 (sequencing):** Ship as a single PR rather than a stacked pair.

</details>

---
Coder Agents generated on behalf of @kylecarbs.
This commit is contained in:
Kyle Carberry
2026-06-22 19:26:34 -06:00
committed by GitHub
parent ce190b3e62
commit cd56ab9e33
59 changed files with 916 additions and 8617 deletions
@@ -1,7 +1,7 @@
package coderd
import (
"fmt"
"database/sql"
"testing"
"github.com/google/uuid"
@@ -16,66 +16,6 @@ import (
"github.com/coder/coder/v2/testutil"
)
func TestActiveAgentChatDefinitionsAgree(t *testing.T) {
t.Parallel()
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitMedium))
db, _ := dbtestutil.NewDB(t)
org, err := db.GetDefaultOrganization(ctx)
require.NoError(t, err)
owner := dbgen.User(t, db, database.User{})
workspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: owner.ID,
}).WithAgent().Do()
modelConfig := insertAgentChatTestModelConfig(t, db, owner.ID)
insertedChats := make([]database.Chat, 0, len(database.AllChatStatusValues())*2)
for _, archived := range []bool{false, true} {
for _, status := range database.AllChatStatusValues() {
chat := dbgen.Chat(t, db, database.Chat{
OrganizationID: org.ID,
Status: status,
OwnerID: owner.ID,
LastModelConfigID: modelConfig.ID,
Title: fmt.Sprintf("%s-archived-%t", status, archived),
AgentID: uuid.NullUUID{UUID: workspace.Agents[0].ID, Valid: true},
})
if archived {
_, err = db.ArchiveChatByID(ctx, chat.ID)
require.NoError(t, err)
chat, err = db.GetChatByID(ctx, chat.ID)
require.NoError(t, err)
}
insertedChats = append(insertedChats, chat)
}
}
activeChats, err := db.GetActiveChatsByAgentID(ctx, workspace.Agents[0].ID)
require.NoError(t, err)
activeByID := make(map[uuid.UUID]bool, len(activeChats))
for _, chat := range activeChats {
activeByID[chat.ID] = true
}
for _, chat := range insertedChats {
require.Equalf(
t,
isActiveAgentChat(chat),
activeByID[chat.ID],
"status=%s archived=%t",
chat.Status,
chat.Archived,
)
}
}
func TestActiveAgentChatsIncludeInheritedACLs(t *testing.T) {
t.Parallel()
@@ -157,3 +97,31 @@ func TestActiveAgentChatsIncludeInheritedACLs(t *testing.T) {
require.Equal(t, rootUserACL, fetchedChild.UserACL)
require.Equal(t, rootGroupACL, fetchedChild.GroupACL)
}
func insertAgentChatTestModelConfig(
t testing.TB,
db database.Store,
userID uuid.UUID,
) database.ChatModelConfig {
t.Helper()
createdBy := uuid.NullUUID{UUID: userID, Valid: true}
provider := dbgen.AIProvider(t, db, database.AIProvider{
Type: database.AIProviderTypeOpenai,
Name: "test-openai",
DisplayName: sql.NullString{String: "OpenAI", Valid: true},
})
dbgen.AIProviderKey(t, db, database.AIProviderKey{
ProviderID: provider.ID,
APIKey: "test-api-key",
})
return dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
Provider: "openai",
AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true},
CreatedBy: createdBy,
UpdatedBy: createdBy,
IsDefault: true,
})
}