> This Pull Request was updated by Mux working on behalf of Mike.
Adds workspace skills to the agent chat slash menu, sourced entirely
from the chat's pinned context resources (the single-chat GET response
the page already fetches), the same inventory `read_skill` resolves
from. No new API endpoint is introduced.
Personal entries insert `/name`, or `/personal/name` when the name
collides with a workspace skill or the chat's pinned context has not
resolved yet; workspace entries insert `/workspace/name`. Qualified
aliases stay searchable even when the displayed trigger is bare. Before
a chat binds a workspace (new chat form, or a selected but unbound
workspace), the menu lists personal skills only.
Sending a message invalidates the chat detail query, and chatd
broadcasts a context watch event when a first-turn bind pins the chat,
so the menu picks up newly pinned context without a reload.
Makes `UpdateChatWorkspaceBinding` a no-op when the requested
workspace/build/agent binding is unchanged, preserving `updated_at` so
chat list ordering and watch events stay stable.
Includes regression coverage for the no-op binding guard, pinned-context
skill mapping, collision qualification, and skills menu behavior.
Refs
[CODAGT-474](https://linear.app/codercom/issue/CODAGT-474/ux-improvements-for-coder-agents)
(skills autocompleting in the editor).
> Mux is working on behalf of Mike.
## Summary
Add a per-user synthetic API key for chatd AI Gateway attribution. Chatd
resolves the key from the chat owner, extends it before expiry, and
discards the generated bearer token so the key is never a usable
credential.
There is no mapping table. The key is resolved from `api_keys` by a
deterministic token name (`chatd_<owner_id>_session_token`), mirroring
the provisionerd session token model, with three deltas that chatd
needs:
- **Login type guard**: token names are unvalidated user input, so a
user can create a bearer token with the colliding name. The lookup
excludes `login_type = 'token'` rows, so chatd never picks up (or
extends) a real user token. Synthetic keys are minted with the owner's
login type, which is never `token`.
- **In-place expiry extension instead of delete-and-reinsert**: chat
generations have no stop boundary, and an in-flight generation may have
already delegated the current key ID to aibridged. Extending
`expires_at` keeps the key ID stable forever.
- **Advisory-lock mint**: the unique index on token names is partial
(`WHERE login_type = 'token'`), so nothing DB-enforces uniqueness for
synthetic keys. A per-user advisory lock serializes concurrent mints.
Keys carry a minimal scope (`api_key:read`) as defense in depth; the
delegated gateway path never evaluates scopes and the secret is
discarded at mint.
Migration 000544 removes the foreign keys from the legacy message and
queue `api_key_id` columns while chatd continues stamping them for
rolling compatibility. Stale IDs are tolerated because routing uses
`chats.owner_id`. Individual key deletion, delete-all, and password
reset remove the key without changing chat history or queue versions,
and the next lookup remints it. Suspension does not delete the key;
delegated gateway authorization rejects inactive users at request time.
This is the first PR in a three-PR rollout and must be fully deployed
before #27171.
Refs
https://linear.app/codercom/issue/CODAGT-561/maintain-synthetic-api-key-per-user-per-chat
Closes CODAGT-721
Closes CODAGT-722
Closes CODAGT-723
Closes CODAGT-724
Closes CODAGT-725
This PR adds the database and API pieces necessary to support full-text
chat message search.
- Adds required chat schema for full-text search
- Adds dbpurge job to populate search_tsv in the background
- Adds `search` parameter to GetChats query
- Adds `search` filter to `searchquery.Chats`
- Wires chat search filter into chats API
> Implemented by Coder Agents, reviewed and tested by a human.
Closes
[CODAGT-792](https://linear.app/codercom/issue/CODAGT-792/handle-revoked-oauth-grants-for-mcp-servers-gracefully).
When a user revokes an upstream OAuth grant for an MCP server used by
Coder Agents, Coder kept treating the cached token as valid:
`invalid_grant` refresh failures were logged and swallowed, the dead
bearer token kept being attached, the list endpoints re-attempted the
refresh on every call, and the UI kept showing the server as
authenticated.
## Changes
Backend, mirroring the `external_auth_links` prior art:
- New migration adds
`mcp_server_user_tokens.oauth_refresh_failure_reason`.
`UpsertMCPServerUserToken` clears it, so completing the OAuth flow again
recovers the row.
- New `MarkMCPServerUserTokenRefreshFailure` query records the failure
and clears all token material, guarded by an `updated_at` optimistic
lock so a stale failure never clobbers a concurrently refreshed token
(on a lock miss the winner's row is used).
- `mcpclient.IsPermanentRefreshError` classifies `*oauth2.RetrieveError`
codes: only `invalid_grant` and `bad_refresh_token` are permanent.
Client/config errors (`invalid_client`, `unauthorized_client`, ...) stay
transient for the user row since reconnecting cannot fix them.
- chatd token refresh and the MCP list/get endpoints persist permanent
failures, return cleared tokens for the in-flight request, and skip
provider calls for already-failed rows.
- `buildAuthHeaders` no longer attaches an Authorization header for
failed tokens, so chat degrades by omitting that server's tools instead
of sending a dead bearer.
API and UI:
- No new API surface. A permanently failed token simply reports
`auth_connected: false`, so the existing "Auth" button and "Not
authenticated" tooltip appear and the user re-runs the same OAuth flow
to recover. An earlier revision added an `auth_status` enum (`connected`
/ `not_connected` / `reconnect_required`) with a dedicated "Reconnect"
button; it was collapsed to keep the API minimal since both states lead
to the identical re-auth action.
Out of scope (follow-up): typed 401-on-connect detection and forced
refresh. mcp-go exposes no stable typed 401 signal in the static-header
path, so a revocation while the access token still looks valid locally
stays undetected until expiry triggers a refresh.
## Testing
- Unit and integration tests: classifier, chatd refresh paths
(permanent/transient/race/persist-failure), API endpoints (revoked,
transient, no-retry caching, re-auth recovery, stale-lock), dbauthz,
dbcrypt, migrations.
- Dogfood UAT against a dev instance with a mock IdP returning
`invalid_grant`: revoked grant detected on refresh and persisted once
(no repeated IdP calls), chat with the revoked server selected completes
with the server's tools omitted, and re-auth restores the connected
state.
> This PR was authored by Mux, working on Mike's behalf.
Closes CODAGT-736
Concurrent chat model config writes on a deployment with no default all
elect themselves default: at READ COMMITTED neither transaction sees the
other's uncommitted default, so both self-promote and
`idx_chat_model_configs_single_default` rejects the loser as a spurious
409. The coderd Terraform provider hits this routinely, since a single
`terraform apply` creates or deletes many configs in parallel by design.
The fix serializes the election with a transaction-scoped advisory lock:
the create, update, and delete handlers run their default election
inside a transaction that first takes `pg_advisory_xact_lock` on a
dedicated `LockIDChatModelConfigDefault`, so elections run one at a time
and the index is never contended. The partial unique index stays in
place as the schema-level invariant, and the existing 409 mapping
remains as a backstop for any writer that bypasses the lock.
We considered a singleton pointer table (one row holding a
`model_config_id` FK, making a second default unrepresentable), which
would remove the race outright, but it needs a migration, new queries,
dbauthz rules, and handler/read-path rework. Not proportionate for an
experimental endpoint.
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Closes
[DEVEX-381](https://linear.app/codercom/issue/DEVEX-381/flake-test-tasksendwaitsforworkingappstate).
Follow-up to #25648 and #25858, which addressed a different symptom of
the same test.
## Symptom
```
task_send_test.go:348: context expired while waiting for trap: context deadline exceeded
--- FAIL: Test_TaskSend/WaitsForWorkingAppState (26.02s)
```
Windows-only, on `test-go-pg (windows-2022)`. Reported four times since
#25648 landed (2026-06-02, 2026-06-10, 2026-07-01).
## Root cause
The test:
1. `setupCLITaskTest` inserts `workspace_app_status(state=idle)` at the
end of setup.
2. `WaitsForWorkingAppState` then inserts
`workspace_app_status(state=working)` before starting the CLI.
3. Both are persisted via `dbtime.Now()`, which rounds to microseconds.
Windows `time.Now()` resolution is coarser than that (often ~1 ms or
worse), so back-to-back calls frequently round to the same microsecond.
4. `GetLatestWorkspaceAppStatusesByWorkspaceIDs` has no tiebreaker:
```sql
ORDER BY workspace_id, created_at DESC
```
Its sibling `GetLatestWorkspaceAppStatusByAppID` already uses `ORDER BY
created_at DESC, id DESC` for exactly this reason. When the two rows
collide, Postgres picks either.
5. On the failing runs, the query returned the `idle` row.
`waitForTaskIdle` saw idle on the first poll, returned nil, `TaskSend`
proceeded, and the CLI completed successfully in ~5 s.
6. But the test was blocked at `resetTrap.MustWait(ctx)` waiting for a
**second** `ticker.Reset` that never happened. `WaitLong = 25s` elapsed,
line 348 failed.
CI log confirms the sequence: only one `Ticker.Reset(5s)` is caught,
then `Ticker.Stop([]) call, matched 0 traps` (from `defer
ticker.Stop()`), then the trap wait times out.
This is the same class of flake Spike documented in #15923 and #21332
("Windows in particular doesn't have high-resolution timers"), just
hidden behind a SQL `ORDER BY`.
## Fix
Two changes:
1. **`coderd/database/queries/workspaceapps.sql`**: add an `id DESC`
tiebreaker to `GetLatestWorkspaceAppStatusesByWorkspaceIDs`, matching
`GetLatestWorkspaceAppStatusByAppID`. Makes the query deterministic when
`created_at` collides.
2. **`cli/task_test.go` / `cli/task_send_test.go`**: add a
`withoutInitialAppStatus()` option to `setupCLITaskTest` and use it from
`WaitsForWorkingAppState`. The test now inserts a single `working` row,
so the collision cannot happen in the first place. Belt-and-braces with
change 1.
Comments in both places reference DEVEX-381 and #21332 so the next agent
doesn't have to re-derive this.
## Verification
- `go test ./cli -run 'Test_TaskSend' -count=1`: all 12 subtests pass,
`WaitsForWorkingAppState` completes in ~5.6 s (was ~16 s previously due
to a longer poll loop).
- Stress: 20 sequential runs of `WaitsForWorkingAppState` on Linux,
race-enabled binary, all pass in ~5.5 s each.
- `go test ./coderd -run 'AppStatus|Task' -count=1` passes.
- `go vet ./coderd/database/... ./cli/...` clean.
- `make lint/emdash` clean.
- `gofmt` clean.
Not reproducible on Linux (real time between the two patches is orders
of magnitude larger than microsecond); the Windows path is fixed by
making the ordering deterministic and by not creating the collision in
the first place.
<details>
<summary>Implementation plan & decision log</summary>
### Investigation
1. Pulled the failing job log for run `28483879823/job/84428355669`.
2. Traced the mock-clock trap sequence: one `NewTicker` and exactly one
`Ticker.Reset(5s)` were caught, then `Ticker.Stop([]) call, matched 0
traps` fires (the `defer ticker.Stop()` on `waitForTaskIdle` return).
This proves `waitForTaskIdle` returned after a single poll, not that the
trap machinery hung.
3. The command exited with `<nil>` (`clitest.go:299: command "coder task
send" exited with error: <nil>`) and a `POST /send` completed in 5.4 s.
So the CLI succeeded; the test's own trap wait is what timed out.
4. The only `waitForTaskIdle` return-nil paths are `Active +
CurrentState.State in {Idle, Complete, Failed}` and `Active +
CurrentState == nil past 30s grace`. First observation of nil cannot be
past 30s. So `TaskByID` must have returned `State == Idle`.
5. Traced `TaskByID` → `taskGet` → `workspaceData` →
`GetLatestWorkspaceAppStatusesByWorkspaceIDs`. Found the missing
tiebreaker; the sibling query one line above
(`GetLatestWorkspaceAppStatusByAppID`) already had it.
6. Confirmed the two `PATCH /app-status` calls in the Windows log
happened at `00:26:13.077` and `00:26:13.093`, well within Windows timer
resolution.
7. Confirmed `dbtime.Now()` rounds to microseconds; Windows `time.Now()`
doesn't have that precision, so `Round(time.Microsecond)` on two calls
close together frequently produces equal values.
### Prior art from Spike
- #15923: loosened `HeartbeatPeriod * 9/10` to `3/4` for Windows.
- #21332: switched `assert.After` to `assert.NotBefore` because
timestamps can equal on Windows.
Both explicitly cite "Windows doesn't always have high-resolution timers
available."
### Considered alternatives
- **Only fix the test.** Works today but leaves the SQL query
non-deterministic; another test that relies on
`GetLatestWorkspaceAppStatusesByWorkspaceIDs` could hit the same
collision.
- **Only fix the SQL query.** Would give a stable answer but not
necessarily the *right* one. If both patches share a `created_at`, `id
DESC` picks whichever UUID sorted higher, still random with respect to
insertion order.
- **Make `dbtime.Now()` monotonic per process.** Cleanest at the source,
but affects every timestamp in the database and has broader implications
than a targeted flake fix.
Going with both the query fix (defense in depth, matches existing
pattern) and the test fix (eliminates the collision at the source) is
the smallest change that closes the flake and hardens the query.
### Rejected commit-message scopes
Changes touch both `cli/` and `coderd/database/`, so per AGENTS.md the
scope is omitted for the cross-cutting commit and PR title.
</details>
The chatd state machine only recognizes `waiting`, `running`, `error`,
`requires_action`, and `interrupting`. Remove the unused `pending`,
`paused`, and `completed` values from the database enum, backend, SDK,
frontend, generated queries, and API docs.
Migration `000543_chat_status_remove_unused` remaps existing `pending`
rows to `running`, remaps `paused` and `completed` rows to `waiting`,
drops the obsolete `idx_chats_pending` index, and recreates
`chats_expanded` around the enum swap. It also removes the dead
`AcquireChats` query and all remaining query literals for the deleted
statuses.
**NOTE**: The enum swap can break chat queries from older replicas
during a mixed-version rollout because they still reference
`'pending'::chat_status`. Chats are experimental, so this PR accepts
that limited rollout window instead of adding a two-release expand and
contract sequence.
> This PR was authored by Mux (AI agent) on Mike's behalf.
Categorises the terminal error of a failed interception and persists it
on the interception record, then surfaces it on the AI Gateway API.
- Categorise into an enum (`bad_request`, `unauthorized`,
`rate_limited`, `overloaded`, `server_error`, `unknown`), unwrapping
the ResponseError envelope, the upstream Anthropic/OpenAI SDK errors,
and key-pool exhaustion so blocking and streaming paths agree.
- Thread the type and raw message through the recorder dRPC into the
`aibridge_interceptions` row (optional proto fields; NULL on success).
- Expose the error on the AI Gateway thread API from the root
interception.
*This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
Adds a nullable `aibridge_interception_error_type` enum and an
`error_message` column to `aibridge_interceptions`, so a failed
interception's terminal upstream error can be persisted.
Schema only: the write path and API exposure land in the stacked
backend PR.
*This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
Closes https://linear.app/codercom/issue/CODAGT-268
## Problem
The chat UI collapses large pastes (>=10 lines or >=1000 chars) into a
synthetic `pasted-text-*.txt` attachment. A chat created with only such
an attachment had no title input anywhere: the create path derived
`titleSource` only from text and file-reference parts (so the chat was
named "New Chat"), async auto-titling extracted text the same way and
silently skipped generation, and the manual propose/regenerate paths
returned an empty title for the same reason. The regular prompt path
already inlines these files for the model; only the title paths were
blind.
## Fix
Add a single title-input derivation in `chatprompt` and use it
everywhere:
- `chatprompt.TitleText` joins text and file-reference parts (unchanged
formatting), and falls back to synthetic pasted-text attachment content
(truncated to a 16 KiB title budget) when they yield nothing.
- `chatprompt.SyntheticPasteFileIDs` identifies paste attachments;
`chatprompt.FallbackTitle` consolidates the previously duplicated
`chatTitleFromMessage` / `fallbackChatTitle`.
- Chat creation captures paste blob references while validating file
parts (the file row was already loaded there) and derives `titleSource`
via `TitleText`. Only the create path derives titles; message send and
edit reuse the same validation without copying any blob data.
- `GenerateChatTitleAsync` and the manual propose/regenerate paths
resolve paste content via `titlePasteText`, which only queries when a
visible user message has no other title text, so chats with typed text
never incur a file fetch.
- Title-path paste fetches are bounded: a new
`GetChatFileDataPrefixesByIDs` query returns only a `substr` prefix
(`chatprompt.TitlePasteBytePrefix`, 64 KiB = 4 bytes x the 16 Ki-rune
title budget) so full blobs (up to 10 MiB each) never leave the database
for titling, and `chatprompt.TitlePasteText` applies the same bound to
the create path which already holds the loaded row.
Deliberate side effect: because generation-time extraction now matches
create-time `titleSource` exactly, file-reference-only chats also become
eligible for AI titles. They were previously skipped by the same
derivation mismatch.
Non-goals: no frontend changes (attachment chip UX stays as is), and
non-synthetic user-uploaded `.txt` files still yield "New Chat".
## Testing
- Unit tests for `TitleText`, `TitlePasteText`, `SyntheticPasteFileIDs`,
`FallbackTitle`, `titleInput`, `titlePasteText`, and paste-aware
`extractManualTitleTurns`.
- Real-database test for `GetChatFileDataPrefixesByIDs` (prefix shorter
and longer than stored data) plus dbauthz coverage for the new query.
- Integration tests: paste-only create gets a fallback title from the
paste content, async title generation fires with the paste content as
input, and `RegenerateChatTitle` works on a paste-only chat.
> This PR was written by [Mux](https://mux.coder.com) on Mike's behalf.
Removes the `UpdateChatMessageByID` query. Its only non-generated
reference was its own dbauthz coverage test, so it is dead code.
> Generated by Coder Agents on behalf of @johnstcn.
This models restart as durable orchestration of existing stop and
start workspace builds instead of adding a new restart transition.
Keeping restart as two existing transitions preserves the current
build/provisioner model.
The child start build is created only after the parent stop build
succeeds, rather than being inserted immediately in a pending
state. That keeps `workspace_builds` aligned with actual
provisioner-ready work and avoids introducing a second
pending-build lifecycle that the provisioner and build acquisition
paths would need to understand.
Refs: https://linear.app/codercom/issue/PLAT-143
User Admin password resets could update the target user's hashed
password but fail while revoking that user's API keys. The transaction
then rolled back and returned HTTP 500, so the password was never
changed.
Add a user-scoped API key revoker actor and use it in both password
reset flows so key revocation succeeds without broader system auth.
Refs: https://linear.app/codercom/issue/PLAT-316
## Problem
The Generate button in the chat Rename dialog (POST
`/api/experimental/chats/{chat}/title/propose`) could fail in ways
unrelated to actual concurrent title generation:
- The manual title lock returned 409 for any `pending` chat and any
`running` chat without a worker. Legacy `pending` rows are never
acquired by workers, so those chats 409'd forever. Running chats are
unowned in the normal window between message submission and worker
acquisition (indefinitely when runners are down), producing spurious
409s.
- A missing default chat model config surfaced as a generic 500, and the
dialog hid the actionable cause carried in the error detail.
## Fix
Backend (`coderd/x/chatd`, `coderd`, `coderd/database`):
- Remove the manual title lock entirely. Races between title writers are
already resolved by `recordManualTitleUsage`, which re-reads the chat
under `GetChatByIDForUpdate` and only persists the generated title when
it is unchanged since the request snapshot, so concurrent regenerates
and renames settle by last write wins. The lock only suppressed
duplicate model calls (the dialog already disables the button in flight,
and usage limits bound spend), and its synthetic `worker_id` marker was
the source of the spurious 409s. The 409 responses, the marker and
staleness handling, and the now-unused
`UpdateChatStatusPreserveUpdatedAt` query are gone.
- New `ErrNoDefaultChatModelConfig` sentinel mapped to 400 "No default
chat model config is configured." in both title endpoints, matching the
POST `/chats` precedent.
Frontend (`site`):
- The Rename dialog error alert now renders the API error detail under
the message, reading `error.response.data.detail` directly so
detail-less API errors do not show the generic developer-console hint.
- Removed the dead regenerate-title UI plumbing (`onRegenerateTitle`
outlet wiring and the `regeneratingTitleChatIds` spinner pipeline). The
Rename dialog propose flow is the only live title-generation UX; the
endpoint, codersdk methods, and the `api.ts`/`queries/chats.ts` layer
are kept for API consumers.
## Tests
- chatd internal: a strict-mock test pinning the compare-and-swap guard
(a concurrently changed title must not be clobbered by a generated one),
plus the existing persist-and-broadcast coverage without lock
transactions.
- HTTP: `PendingWithoutWorker` expects 200 for both endpoints,
`NoDefaultModelConfig` (400) subtests, a stopped-workspace propose
regression, and an `Unauthenticated` propose subtest.
- Storybook: stories asserting the API error detail renders in the
dialog alert, and that detail-less API errors and plain errors do not
leak the developer-console hint.
> Authored by Mux on Mike's behalf.
---------
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Removes OpenAI Responses "chain mode" from chatd. Closes CODAGT-445.
- Deletes `chatopenai/responses.go` (chain detection, activation, prompt filtering, response ID extraction) and its tests.
- Deletes the `ChainBroken` classification in `chaterror` and the chatloop retry bookkeeping that disabled chain mode mid-generation.
- Drops the `chain_broken` label from the `coderd_chatd_stream_retries_total` metric.
- Stops reading and writing `chat_messages.provider_response_id`
- Deletes the dead `ClearChatMessageProviderResponseIDsByChatID` query. Dropping the column is a follow-up migration.
- Deletes three chatloop hooks no caller sets (`ReloadMessages`, `DisableChainMode`, `PrepareMessages`), the dead `const AgentChatContextSentinelPath`, and stale chain-mode comments.
🤖 Generated by Coder Agents on behalf of @johnstcn.
## Summary
Plumbs the Responses output item id (added as `ToolUsageRecord.ItemID` in #26855) through to the database, captured independently of the `provider_tool_call_id` correlation key. Hosted tools (`web_search_call`, etc.) only have an item id; agentic tools have both.
`provider_item_id` is specific to the OpenAI Responses API; it stays empty for chat completions and Anthropic messages, which have no separate item id.
## Changes
- Migration `000534`: nullable `provider_item_id` column on `aibridge_tool_usages`.
- Proto: `item_id` field 11 on `RecordToolUsageRequest`.
- Server handler: persists `provider_item_id` and adds it to structured logging.
- Translator: maps `ToolUsageRecord.ItemID` to the proto field.
## Tests
- `TestRecordToolUsageProviderItemID`: real-database round-trip asserting `provider_item_id` persists for both hosted and agentic tools, independently of `provider_tool_call_id`.
Stacked on #26855. Linear: AIGOV-96
---
_This PR was produced by opencode (agent) using the_ _`anthropic/claude-opus-4-8`_ _model, under human direction and review._
## Description
Adds pre-request AI budget enforcement to `aibridged`. Requests are rejected with HTTP 403 when the user's aggregated spend for the current period has reached their effective limit.
## Changes
- Add `IsBudgetExceeded` RPC to `aibridgedserver`. Resolves the user's effective budget, aggregates spend over the caller-supplied `[period_start, now]` window, and returns whether the limit has been reached along with the effective limit.
- Wire the check into `aibridged`'s HTTP handler. The caller computes the period start (monthly for now) and passes it in the request.
- Reject exceeded requests with HTTP 403 Forbidden and a message directing the user to contact an administrator.
- Add `dbtime.StartOfMonth` alongside `StartOfDay` for period computation.
- Add real-DB tests covering the enforcement path: month-boundary excludes prior-period spend, and a new user override unblocks a previously-exceeded user.
Closes https://linear.app/codercom/issue/AIGOV-428/add-pre-request-budget-enforcement
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
The provider type already lives authoritatively in ai_providers.type,
reachable on every active row through ai_provider_id, which the
chat_model_configs_ai_provider_required_when_active CHECK makes
mandatory. The stored provider string was a denormalized copy the system
kept in sync with a startup backfill and no longer needs.
Every surface now derives provider type from the linked ai_providers
row. Telemetry is the one exception: it keeps emitting provider, now
sourced from ai_providers.type via a JOIN, so the BigQuery column and the
Nexus dashboards that read it are unaffected. The experimental HTTP/SDK
response drops provider and makes ai_provider_id required, since those
endpoints return only active configs; consumers resolve provider type
from ai_provider_id and the AI providers listing.
This ships in a single release with no compatibility window: production
reads the table via SELECT *, so a pre-drop binary fails config reads the
moment the column is gone. Operators must scale to zero before upgrading,
and there is no rollback.
Closes CODAGT-599
Configuring only a GitHub Copilot provider left the Agents page stuck on
"set up a provider then add a model", even with a provider and models
configured. The catalog dropped any provider type that NormalizeProvider
did not recognize, so a Copilot-only deployment looked identical to an
empty one and never unlocked the page.
The Agents harness cannot use Copilot: it needs a per-request token only
an official Copilot client can mint, and the harness is not one. Instead
of dropping such providers, the catalog now reports them as unsupported
so the UI can explain the dead end and point elsewhere, rather than ask
for setup that already happened. The providers stay usable through the
AI Gateway proxy.
Support is derived from the provider type, not stored, so there is no
migration. codersdk.IsAgentsUnsupportedProviderType is the single source
of truth, consulted by the chatd catalog and, through the generated
AgentsUnsupportedProviderTypes list, the frontend.
The diff also carries unrelated modernization of nearby db2sdk and
chatprovider helpers (slices.SortFunc, strings.Cut, range-over-int).
Closes CODAGT-627
Refs CODAGT-256
Refs CODAGT-682
Rename user-facing "AI Bridge" strings to "AI Gateway" in deployment
config, RBAC display names, log messages, error strings, docs style
guide, and Grafana dashboard README.
Deprecated option names and descriptions (the `--aibridge-*` block) are
intentionally kept as "AI Bridge". The `Name` field cannot be renamed
because `serpent` uses it as a unique key during JSON serialization;
duplicating names causes `UnmarshalJSON` failures (e.g. in the support
bundle). Descriptions also stay as "AI Bridge" to avoid confusion
between the deprecated and primary options.
Refs https://linear.app/codercom/issue/AIGOV-226
> Generated with the assistance of Coder Agents (@ssncferreira)
Renames the `last_used_at` column to `last_heartbeat_at` in `ai_gateway_keys` table.
`ai_gateway_keys` table has not been released yet.
All references updated.
Adds DB methods`GetAIGatewayKeyIDByHashedSecret` and `UpdateAIGatewayKeyLastUsedAt`.
`GetAIGatewayKeyIDByHashedSecret` - returns AI Gateway key ID by hashed secret value.
`UpdateAIGatewayKeyLastUsedAt` - updates last used timestamp for given AI Gateway key.
Used by standalone AI Gateway for authentication and keeping track of currently used keys.
relates to GRU-69
Adds cluster_host and nats_port to replicas table, to explicitly track NATS routes in the cluster.
I decided to make the NATS support explicit and transport the port number over the replicasync so that different Coder Servers can run on different ports. This is not something customers will typically care about, but is very useful for testing, so that they can all run on localhost within one machine.
I've also gone with a design where the NATS pubsub directly tells replicasync the port number _after_ it opens the socket. This is also very useful for testing because it allows us to have the OS assign the port number at runtime, avoiding races where we fail to bind to a free port.
## Problem
#23108 made prebuild claim delivery durable: when an agent connects to
`/api/v2/workspaceagents/me/reinit?wait=true`, the handler checks
whether the workspace's first build was created by the prebuilds system
user and whether its latest build succeeded, and if so pre-seeds a
`prebuild_claimed` reinitialization event in case the original pubsub
event was missed.
The check does not verify that the latest build is the claim build, so
it keeps firing for the rest of the workspace's life. Any workspace that
was claimed from a prebuild receives a spurious "prebuild claimed"
reinit every time its agent (re)opens the `/reinit` connection: after
every agent restart, every coderd deploy or replica restart, and every
dropped SSE connection. Each one shuts the agent down and reinitializes
it, killing SSH/IDE sessions and re-running startup scripts. In our
deployment, where most workspaces are claimed from prebuilds, this
caused fleet-wide "agent disconnected" blips whenever a coderd replica
restarted, and a few workspaces whose container exits when the agent
restarts went into a restart loop every 15-60 minutes. The agent-side
dedup (`lastOwnerID` in `cli/agent.go`) only suppresses the second event
within one agent process, so every new agent process takes at least one
spurious restart.
## Fix
Only seed the reinitialization event while the latest build is the claim
build itself, determined from the build job's input
(`prebuilt_workspace_stage`), the same signal `provisionerdserver` uses
when publishing the claim event:
- Latest build is the claim build: behavior unchanged (seed when the job
succeeded, 409 when it failed permanently, wait on pubsub while it is in
progress).
- Latest build is still a prebuilds-initiated build (claim build not
created yet): fall through to the pubsub subscription, which delivers
the claim event when the claim build completes.
- Latest build is any later user-initiated build: the claim was already
handled, so return 409 and the agent stops polling, the same as a
regular workspace.
`dbfake` gains a `MarkPrebuiltWorkspaceClaim()` builder option so tests
can model claim builds' job input, and the existing `TestReinit` claim
subtests now use it. A new subtest covers the long-claimed workspace
case.
One deliberate behavior change worth calling out: if a claim build fails
and the owner retries with another start build, the handler now returns
409 for that retry build rather than seeding a reinit. This matches the
existing treatment of failed claim builds as terminal for the reinit
poller.
## Verification
- `go test ./coderd/ -run TestReinit` against Postgres 17: all subtests
pass, including the new `workspace claimed in the past gets 409` case.
- `gofmt`, `go vet`, and `golangci-lint` (v1.64.8) are clean on the
touched packages.
- The fix mirrors behavior validated by hand against an affected
deployment: for a long-claimed workspace, `/reinit?wait=true` returned
the seeded `prebuild_claimed` event on every connection before the
change and a 409 afterwards.
Note: this branch was prepared in an environment without the full local
toolchain, so the repo's pre-commit hook (`make pre-commit`) was not run
locally; relying on CI for the full gen/fmt/lint suite. Opening as a
draft mainly to report the issue and propose a fix; happy to rework it
to the maintainers' preferred approach.
---------
Co-authored-by: Sas Swart <sas.swart.cdk@gmail.com>
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.
Add `agent_firewall_session_id` and `agent_firewall_sequence_number`
fields to `AIBridgeThread` in the `GET
/api/v2/aibridge/sessions/{session_id}` response. These fields link each
thread to its agent firewall confinement session so the frontend can
discover the boundary session and compute sequence ranges for
interleaving firewall events within the thread timeline.
The database columns already exist on `aibridge_interceptions`
(migration 000520) and are already selected by
`ListAIBridgeSessionThreads`. This PR surfaces them through the SDK type
and the `db2sdk` conversion.
Depends on #24814
**Naming note:** The RFC uses `boundary_session_id` /
`boundary_sequence_number`, but the codebase standardized on
`agent_firewall_*` naming in the DB migration. The API fields follow the
existing convention.
</details>
> [!NOTE]
> This PR was authored by Coder Agents.
Add a `GET /api/v2/agent-firewall/sessions/{id}/logs` endpoint that
returns agent firewall audit logs for a given session, sorted by
sequence number ascending.
The endpoint supports `seq_after` and `seq_before` (exclusive bounds)
and `limit` query parameters. This enables the frontend to fetch exactly
the firewall events that fall between two AI Bridge interceptions within
a thread, as described in FR 4 of the Boundary/Bridge correlation RFC.
Authorization reuses the `boundary_log` RBAC resource (owner and auditor
can read; members cannot). Returns 404 for unauthorized users to avoid
leaking existence information.
The endpoint is enterprise-only, gated behind `FeatureBoundary`
entitlement, matching the session endpoint from #24814.
Depends on #24814
> [!NOTE]
> This PR was authored by Coder Agents.
In our codebase we have an existing convention of using
`flag.Lookup("test.v")` instead of `testing.Testing()`. This avoids
pulling in the entire `testing` package. Another consequence: some of
our custom linters trigger upon import of the `testing` package which
can lead to unexpected linter errors.
The dormancy notification's "will be automatically deleted in X"
sentence rendered the dormancy threshold instead of the auto-delete
duration. A 30-day threshold rendered as "4 weeks" even when auto-delete
was 90 days; a 60-day threshold rendered as "1 month" with a 7-day
auto-delete. Render the countdown from the auto-delete setting, and skip
the deletion sentence entirely when auto-delete is disabled so the
notification no longer promises a deletion that will never happen.
Add a GET endpoint at `/api/v2/agent-firewall/sessions/{id}` that
returns agent firewall session metadata (`id`, `workspace_id`,
`owner_id`, `confined_process`, `started_at`). The handler authorizes
against the `boundary_log` resource with `ActionRead` via dbauthz.
The endpoint is enterprise-only, gated behind the `FeatureBoundary`
entitlement.
The `GetBoundarySessionByID` SQL query JOINs through `workspace_agents`
→ `workspace_resources` → `workspace_builds` → `workspaces` to return
`workspace_id` and `workspace_owner_id` directly, avoiding a separate
query.
Also adds an `owner_id` column to the `boundary_logs` table (migration
000526) with a FK to `users(id)` and a backfill from
`boundary_sessions`. This enables user-scoped RBAC authorization for
`InsertBoundaryLogs` via `.WithOwner()`, ensuring workspace agents can
only insert logs for their own owner.
Depends on #24810
**RBAC behaviour:**
| Role | Result |
|---------|--------|
| Owner | read |
| Auditor | read |
| Member | 404 |
> [!NOTE]
> This PR was authored by Coder Agents.
closes CODAGT-203
## Summary
`list_templates` now returns a ranked shortlist with a recommendation,
so the chat agent can pick the right template the way a colleague would:
prefer what matches the request, what the user already uses, and what
the rest of the organization uses. Instead of teaching the model an enum
protocol in prompts, every result carries a fixed `next_step`
instruction telling the agent what to do.
## How list_templates works
1. **Fetch**: active, non-deprecated templates in the chat's
organization, filtered by the admin template allowlist, authorized as
the chat owner (no system escalation).
2. **Query relevance** (optional `query` argument): each template
receives the highest tier any of its fields matches, and a higher tier
always outranks a lower one regardless of usage:
| Tier | Match |
|------|-------|
| 4 | name or display name equals the query |
| 3 | name or display name starts with the query |
| 2 | name or display name contains the query |
| 1 | description contains the query (checked only when no name field
matched) |
| 0 | no match; the template is excluded |
Matching is case-insensitive and ignores spaces/hyphens/underscores
(`python gpu` matches `python-gpu`).
3. **Usage signals**: a new `GetTemplateRankingSignalsByOwnerID` query
returns, per template, the owner's active and recently-deleted workspace
counts within a 60-day window, the last in-window usage, and the count
of distinct developers with an active workspace (unclaimed prebuilds
excluded).
4. **Affinity score** (computed in Go, per template, from that
template's signals only):
```text
affinity = 10 x (active + 0.5 x deleted) x 0.5^(days_since_last_use /
14)
+ ln(1 + active_developers)
```
`active`/`deleted` are the owner's in-window workspace counts,
`days_since_last_use` is measured from the most recent in-window usage
(the personal term is zero without in-window usage), and
`active_developers` is the org-wide count. Personal usage carries 10x
the weight of org popularity; the confidence floor is the score of two
active developers (`ln 3`) and the required lead over the runner-up is
`ln 3 - ln 2`.
5. **Rank**: query tier first (when a query is present), then affinity
score, then name/ID for determinism. Results paginate 10 per page with
`next_page` present only when more exist.
## Recommendation contract
The result tells the agent what to do next instead of describing
confidence levels:
- `recommended_template_id` is present only when the top template is a
clear winner: the only available template, a decisive query match, or an
affinity score that clears a floor and leads the runner-up by a derived
margin.
- `next_step` is always present and is one of four fixed sentences: use
the recommendation, ask the user to choose, retry a query that matched
nothing, or report that no templates are available.
Per-template items carry raw evidence (`active_developers`,
`your_workspace_count`, `last_used_by_you`) rather than derived labels.
When signals fail to load, the tool logs and degrades to asking the user
unless the query alone is decisive.
Prompts and the `create_workspace`/`read_template` descriptions
reference the field through the `chattool.NextStepField` constant, so
the instruction lives in one place and cannot drift. `create_workspace`
remains idempotent and allowlist-enforced.
## Authorization
The signals query runs with the chat owner's permissions: reading the
owner's own workspaces plus a template-metadata read for the cross-user
popularity count. dbauthz rejects the call if any requested template is
not readable by the owner (covered by allow and deny method tests).
## Docs
Adds `docs/ai-coder/agents/tools/` explaining how agent tool calls work,
with `list_templates` ranking and the `next_step` contract as the first
documented tools.
Make `newInternalTestServer` use option functions for logger, clock, and
worker startup, and make it passive by default so internal chatd tests
only opt into background execution when they need a real worker.
Use the passive server path in `TestAwaitSubagentCompletion` for the
state-driven subtests, keep `ContextCanceled` explicitly active for real
provider cancellation coverage, and keep the fail-fast default AI
provider base URL so accidental provider calls still fail immediately.
Closes CODAGT-586
Closes https://github.com/coder/internal/issues/1549
Removes the coder agents PR Insights page (`/agents/settings/insights`) and all of its backend support. The page had previously been hidden and was only reachable via deep link. It had previously been hidden due to the dubious value provided in the current iteration.