WakeResponse.success was always set to true regardless of whether a
rebind actually happened (e.g. when debounced or the tunnel isn't
running yet), so it carried no useful information. Remove it,
mirroring the earlier removal of the unused error_message field from
the same message.
Closes CODAGT-352
This adds the Coder Agents experiments (virtual desktop with computer
use, and the advisor) to telemetry, so they finally show up in each
deployment snapshot. Everything stays inside `coderd/telemetry/`.
## Shape received by the telemetry server
The experiments are reported as a single `agents_experiments` field on
the deployment record, alongside the other config-derived deployment
fields. Its value is one JSON blob with one top-level key per
experiment:
```json
{
"virtual_desktop": {
"enabled": false,
"computer_use": {"provider": "anthropic", "provider_source": "default"}
},
"advisor": {"enabled": true, "max_uses_per_run": 5, "max_output_tokens": 4096, "provider": "openai", "model": "gpt-5.2"}
}
```
When the advisor falls back to the chat model, either because no
override is set or because the configured override is inactive (its
config or provider was deleted or disabled), the provider and model
carry a sentinel instead:
```json
"advisor": {"enabled": true, "max_uses_per_run": 5, "max_output_tokens": 4096, "provider": "advisor_reuse_chat_model", "model": "advisor_reuse_chat_model"}
```
- `virtual_desktop.enabled` and `advisor.enabled` track the
`chat-virtual-desktop` and `chat-advisor` deployment experiments, not
the stored config. We ignore the stored advisor `enabled` flag on
purpose: since #26809 the runtime gates on the experiment, and the
stored flag ends up permanently true for any deployment that ever opened
the settings form.
- Computer use sits under `virtual_desktop` rather than as its own
top-level key because it isn't a separate experiment; the same
`chat-virtual-desktop` flag gates both the desktop and the computer-use
provider. `provider_source` says whether an admin picked the provider
(`configured`) or we fell back to the default (`default`).
- `advisor.provider` is the `ai_providers` type (e.g. `openai`,
`anthropic`, `azure`) and `advisor.model` is the configured model
string. Two sentinels stand in when there's no concrete value:
`advisor_reuse_chat_model` when the advisor has no active override and
falls back to the chat model (matching the runtime), and `unknown` when
we genuinely couldn't tell, e.g. a query failed or the stored config
wouldn't parse.
- `advisor.max_uses_per_run` and `advisor.max_output_tokens` are clamped
to 0 before reporting, matching how the API normalizes these values on
read.
## Why this shape
Putting the data on the deployment record keeps it next to the other
config-derived fields, and leaves `telemetry_items` as a faithful mirror
of the `telemetry_items` table rather than a place we inject synthetic
rows. Adding or removing an experiment is a one-line edit to the
`agentsExperiments` registry. The `agents_experiments` field itself
never changes; only the JSON inside it does. The field is `omitempty`,
so older Coder versions that don't emit it are distinguishable from a
real absence, and when an experiment isn't reported in a snapshot its
JSON path is simply missing, so queries can tell "not reported" apart
from a real `false`.
One key holding one JSON blob is also easier to query than many separate
fields. Because everything lives in one blob, a question like "of the
deployments running the desktop, how many changed the computer-use
provider?" is one query with no join:
```sql
SELECT
JSON_VALUE(agents_experiments, '$.virtual_desktop.computer_use.provider_source') AS src,
COUNT(*) AS deployments
FROM deployments
WHERE JSON_VALUE(agents_experiments, '$.virtual_desktop.enabled') = 'true'
GROUP BY src
```
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.
Adds `ChatModelCallConfig.UnmarshalStrict`: `UnmarshalJSON` except
unknown fields and trailing data are errors instead of being silently
dropped.
`model_config` is free-form JSON at its edges (Terraform config, API
bodies), so a typo'd setting is dropped with no signal;
coder/terraform-provider-coderd#388 uses this for plan-time validation.
It has to live in codersdk because the custom `UnmarshalJSON` and its
unexported aux struct (which defines the accepted key set, including
legacy pricing aliases) make strict decoding impossible from outside the
package. `UnmarshalJSON` stays lenient since it is on the read path for
stored configs and older clients, where unknown keys mean version skew
rather than user error.
_Opened by Coder Agents on behalf of @ethanndickson._
Relates to CODAGT-797
> 🤖 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>
Bumps the coder-modules group with 1 update in the /dogfood/coder
directory: coder/code-server/coder.
Bumps the coder-modules group with 1 update in the
/dogfood/coder-envbuilder directory: coder/code-server/coder.
Bumps the coder-modules group with 1 update in the /dogfood/vscode-coder
directory: coder/code-server/coder.
Updates `coder/code-server/coder` from 1.5.0 to 1.5.1
Updates `coder/code-server/coder` from 1.5.0 to 1.5.1
Updates `coder/code-server/coder` from 1.5.0 to 1.5.1
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Implements
https://linear.app/codercom/issue/AIGOV-213/add-bedrock-provider
# AWS Bedrock mantle support in AI Gateway
## Summary
Add support for the AWS Bedrock **mantle** endpoint
(`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`) to AI Gateway.
Mantle serves Claude through the native Anthropic Messages API. We model
it as a `protocol` field on the existing Bedrock provider settings
(`invoke-model` default, or `mantle`) rather than as a new provider
type, and we treat mantle as a pure passthrough: SigV4-sign and forward,
no body translation.
## Background
Claude on AWS Bedrock is reachable through two endpoints, each speaking
exactly one wire protocol:
1. **InvokeModel** (existing): `bedrock-runtime.{region}.amazonaws.com`.
Model id in the URL path, request translated into Bedrock's InvokeModel
format, responses returned as a binary AWS eventstream. This is what AI
Gateway already supported for Bedrock.
2. **Mantle** (this doc):
`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`. Native
Anthropic Messages API: model in the body, plain SSE streaming.
## Why a `protocol` field, not a new provider type
The alternative is to model mantle as its own `ai_provider_type`
(`bedrock-mantle`) alongside `bedrock`. I chose the `protocol` field
instead for two reasons:
1. Mantle reads more like a protocol of Bedrock than a separate
provider. It is the same AWS account, credentials, region, and IAM,
reached over a different wire protocol and host. One Bedrock provider
with two protocols (`invoke-model` default and `mantle`) models that
more organically than two provider types.
2. It avoids a database migration. The `protocol` field lives in the
settings JSON blob (empty resolves to `invoke-model`, so existing
providers are unaffected), whereas a new type means an enum value and
the `ALTER TYPE ... ADD VALUE` migration that goes with it.
## Why passthrough, not translation
The client already emits Bedrock-legal requests in mantle mode:
```sh
export CLAUDE_CODE_USE_MANTLE=1
export CLAUDE_CODE_SKIP_MANTLE_AUTH=1
export ANTHROPIC_BEDROCK_MANTLE_BASE_URL=https://<coder>/api/v2/aibridge/<provider-name>
```
So the gateway just forwards the body and SigV4-signs it (service
`bedrock-mantle`), and skips all the InvokeModel body-translation (model
remap, thinking conversion, beta-flag allowlist, field stripping). This
keeps the mantle path thin and avoids a second copy of translation logic
to maintain.
## Consequences
- Protocol-dependent fields: `model` / `small_fast_model` are used by
InvokeModel but ignored by mantle (the client sends the model), and
`base_url` is required for mantle but optional for InvokeModel.
Validation is protocol-aware.
- No central model control on mantle: because it is a passthrough, the
operator cannot pin the model.
- `region` and the `base_url` host must name the same region (the SigV4
scope must match the endpoint); a mismatch surfaces as `Credential
should be scoped to a valid region`.
## Draft UI
<img width="1100" height="579" alt="image"
src="https://github.com/user-attachments/assets/37bab46d-8958-4a96-9f47-1fef3493e1b6"
/>
## Follow-up PRs:
- https://github.com/coder/coder/pull/27156
## Problem
The desktop `DeploymentDropdown` surfaces **AI** and **AI sessions**
items under Admin settings (behind `canViewAISettings` /
`canViewAIBridge`), but the mobile `MobileMenu` was never updated to
match — its `AdminSettingsSub` only knows about Deployment,
Organizations, Audit logs, Connection logs, and Healthcheck.
This is a leftover from #25582 ("promote AI settings to a top-level
section"), which threaded `canViewAISettings` through `Navbar` /
`NavbarView` / `DeploymentDropdown` but did not touch `MobileMenu.tsx`.
`canViewAIBridge` has the same oversight.
## Fix
- Extend `MobileMenuPermissions` with `canViewAIBridge` and
`canViewAISettings`.
- Render **AI** (→ `/ai/settings`) and **AI Sessions** (→
`/ai-gateway/sessions`) in the mobile Admin settings collapsible, in the
same order as the desktop dropdown.
- Thread the two flags through `NavbarView` into `MobileMenu`.
- Cover the new args in `MobileMenu.stories.tsx` (Admin story now shows
the AI items; Auditor / OrgAdmin / Member keep them hidden).
No backend, permission, or routing changes.
> 🤖 This PR was drafted by Coder Agents on behalf of @tracyjohnsonux and
needs a human review.
fixes DEVEX-463
Previously, a dynamic parameter of `form_type = "multi-select"` with
default options selected would only pass in these 3 properties for each
option fed into `MultiSelectCombobox`'s `defaultOptions`:
- `value`
- `label`
- `disable`
`defaultOptions` are used as the initial value of
`MultiSelectCombobox`'s `selected` array of `Option`s. If the `icon`
property is missing from a selected option, then no icon is shown:
https://github.com/coder/coder/blob/e59a67d63fd522d1e95b42e57d4fbfe7d2bc5fef/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx#L501-L502
Now `icon` and `description` are included among those properties passed
to default selected options, when a default option's value can be found
among the parameter's available options in the Terraform template. This
fixes the bug where icons wouldn't display for default selected options.
If no corresponding option is found, then we fall back to the old
partial option object (`{ value, label, disable }`).
<img width="1840" height="1191" alt="Screenshot 2026-07-09 at 10 08
54 PM"
src="https://github.com/user-attachments/assets/91be4aaf-9016-46d6-871b-344c8f2504cb"
/>
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.
## Description
Read the AI budget period from the deployment config on both the RPC server and the `/users/{user}/ai/spend` endpoint, instead of hardcoding `month`. Drops the `period_start` RPC parameter that was incorrectly introduced in #26915: the period should have been derived from the deployment config from the start.
## Changes
- Add `codersdk.NewAIBudgetPeriodFromString`, mirroring `NewAIBudgetPolicyFromString`.
- `aibridgedserver.Server` takes a `quartz.Clock`, reads `BudgetPeriod` from the deployment config at construction, and computes the period window inside `IsBudgetExceeded`.
- Remove `period_start` from `IsBudgetExceededRequest` and stop sending it from the daemon.
- The `userAISpendStatus` endpoint reads the period from `AIBridgeConfig.BudgetPeriod` instead of hardcoding month.
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
## Summary
Fixes two classes of invalid/broken HTML in hand-written docs. Both are
visible problems in today's rendered docs, independent of any
docs-engine work.
1. **`</br>` is not a real HTML tag.** `br` is a void element with no
closing form; browsers error-correct `</br>`, but it is invalid HTML.
Replaced all 15 usages with `<br />` across:
- `docs/admin/templates/extending-templates/dynamic-parameters.md`
- `docs/admin/users/idp-sync.md`
- `docs/tutorials/best-practices/organizations.md`
2. **Browser-swallowed placeholder URL.** In
`docs/ai-coder/github-to-tasks.md`,
`https://<your-coder-url>/settings/external-auth` was unformatted, so
HTML renderers parse `<your-coder-url>` as an unknown tag and drop it.
The live docs currently render the broken text `re-authenticate at
https:///settings/external-auth`. Wrapped in backticks, matching every
other instance in the same file.
Table realignment noise in the diff is from `fmt/markdown` (`<br />` is
one character wider than `</br>`).
A repo-wide grep confirms no remaining `</br>` and no other unformatted
`https://<placeholder>` URLs in prose (other hits are inside code fences
or already backticked). The equivalent placeholder issues in
**generated** reference docs (CLI help strings, swagger annotations) are
intentionally out of scope and tracked separately in
[DOCS-551](https://linear.app/codercom/issue/DOCS-551/backtick-placeholder-syntax-in-generated-reference-docs-cli-help).
Tracking issue:
[DOCS-550](https://linear.app/codercom/issue/DOCS-550/fix-invalid-br-tags-and-browser-swallowed-placeholder-url-in-hand)
---
Created by Coder Agents on behalf of @nickvigilante.
Finishing touches for the AI cost control group members table.
- Spend stays primary-colored until near the budget limit, via a new
AIBudgetAmount component
- "AI budget period" label shows the current spend window in local
time, next to the members tab
- Budget tooltip notes the reset date and the group's default limit
- "Budget type" renamed to "Budget group", with a badge for the
governing group or override
- Distinguishes $0 budget ("None") from no budget ("Unlimited")
- Unattributed spend from another group shows a note instead of a dash
- "Manage AI budget" disabled only when another named group governs
- Replaces UserAISpend with generated UserAISpendStatus, fixing
limit_source
Closes#26401
## Summary
Part of the docs front-matter title migration (Linear DOCS-482).
`offlinedocs` now prefers a front-matter `title` for a page and falls
back to the
manifest nav label when the page has no front matter. This keeps the
offline docs
renderer aligned with the hosted docs renderer as docs pages migrate to
front-matter
titles.
No `docs/**` page has front matter today, so every page renders exactly
as before.
The existing `& + h1` dedup is kept, so exactly one H1 renders.
## Changes
- `offlinedocs/pages/[[...slug]].tsx`
- `getStaticProps` reads `attributes` from `front-matter` and passes a
resolved
`title` (front matter, else manifest label) through props.
- The `<title>` and the injected page `<Heading>` render the resolved
title.
## Verification
Built offlinedocs (`pnpm build`): 455/455 static pages generated.
Temporarily added a
front-matter title to one page to confirm precedence, then reverted:
| State | `<title>` | page `<h1>` |
|---|---|---|
| Before (no front matter) | `Administration` | `Administration` |
| After (`title: "FM Proof: Administration Console"`) | `FM Proof:
Administration Console` | `FM Proof: Administration Console` |
The body `# Administration` stayed hidden by the `& + h1` dedup, so
exactly one H1
rendered in both cases. `pnpm lint` (`tsc --noEmit`) and `prettier
--check` both pass.
## AI disclosure
This PR was generated by Coder Agents and opened on behalf of
@nickvigilante, who is
accountable for its contents. Manual verification evidence is included
above per the
[AI contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING).
Fixes flake reported in
[DEVEX-538](https://linear.app/codercom/issue/DEVEX-538/flake-create-user-with-password).
## Problem
The `login()` e2e helper had a race condition causing intermittent
navigation failures:
```
page.goto: Navigation to "/deployment/users" is interrupted by
another navigation to "/"
```
After clicking Sign In, `LoginPage.tsx` does a hard navigation via
`location.href = sanitizeRedirect(redirectTo)`. With no `?redirect=`
param, `retrieveRedirect` defaults to `"/"`, so login navigates to `/`.
The browser loads `/`, fires the `load` event, then React boots and the
router does a client-side redirect from `/` to `/workspaces` (via
`<Navigate to="/workspaces" replace />`).
The old helper waited with
`expectUrl(page).toHavePathName("/workspaces")`, which polls
`page.url()` and resolves the moment the pathname matches. It has no
awareness of page load state. So it resolved after the client-side
redirect changed the URL, but before the `/workspaces` page components
had mounted. When a test immediately called `page.goto()` afterward,
pending React rendering could trigger a competing navigation.
## Fix
Replace the URL polling with two Playwright-idiomatic waits:
1. `page.waitForURL(/\/workspaces/)` hooks into the browser's navigation
lifecycle: it waits for the URL to match AND for the page to reach a
load state (`"load"` by default), unlike `expectUrl` which is purely a
string poll.
2. `await expect(page).toHaveTitle(/Workspaces/)` waits for the page
title, which is set by the `WorkspacesPage` component. This proves React
booted, auth resolved, and the page fully rendered, closing the window
where pending React work could interfere with the next navigation.
Also adds `{ waitUntil: "domcontentloaded" }` to `page.goto("/login")`
for consistency with every other navigation helper in the file.
> 🤖 Generated by Coder Agents on behalf of @jeremyruppel
Add a CoderVPN WakeRequest RPC so Coder Desktop can trigger the
existing link-change recovery path (Rebind + ReSTUN) immediately on
OS wake, instead of waiting for magicsock's idle re-STUN timer. Wake
events are debounced to at most one rebind per 5s to avoid duplicate
resets of peer path trust.
Closes#26736
The deployment-wide computer use provider was passed around as a bare `string` on the `codersdk` wire structs, in `chattool`, and in the generated TypeScript, and its valid values (`anthropic`, `openai`) were never exposed as a `codersdk` enum. That's out of step with our other chat settings (`ChatDebugRunKind`, `ChatUsageLimitPeriod`), which already define enums with `Valid()` and an `All<Name>s` slice, and it left the allowed values duplicated as literals with no typed contract for clients.
This adds `codersdk.ChatComputerUseProvider` as the single source of truth and routes the API boundary, `chattool`, `chatd`, and the generated TypeScript through it. The DB layer and chattool's internal model-provider routing stay `string` on purpose, since they handle untrusted or fantasy-model values that just happen to share the names.
Coder Agents chats could get stuck showing "Thinking" forever when a
title regenerate/propose request ran while a generation was in flight.
Manual title generation recorded token cost by inserting a hidden
assistant message into `chat_messages` and immediately soft-deleting it.
Triggers on that table sync `chats.history_version` to
`snapshot_version`, so this out-of-band write broke the
`history_version` fence of an in-flight generation task, killing it
without a replacement and leaving the chat stuck in `running`.
Remove the accounting path entirely; AI Gateway already records
title-call usage in `aibridge_interceptions`/`aibridge_token_usages`.
The manual title endpoints no longer write to `chat_messages` at all,
and new regression tests assert `history_version` stays untouched. Note
this intentionally drops title-generation cost from chatd's chat-level
cost surfaces; it still counts against the user's AI budget via AI
Gateway.
Closes CODAGT-595
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
The "Session completed" marker at the bottom of an AI Gateway session
timeline was rendered unconditionally, so on long sessions it appeared
below still-loading threads while the user scrolled. That is misleading:
users read it as the end of the session even when more threads are about
to stream in.
Only render the session end marker (rows 7 and 8 of the grid: the
connecting vertical line, the success dot, and the "Session completed"
text) once every thread has loaded, that is, once both `hasNextPage` and
`isFetchingNextPage` are false. The dashed timeline box still closes
cleanly at the bottom, and the infinite-scroll spinner keeps rendering
inside row 5 while more pages fetch.
| Old | New |
| --- | --- |
| <img width="1099" height="338" alt="preview-old-behaviour"
src="https://github.com/user-attachments/assets/f86c9ce1-f4ca-4088-a1ed-9cdcf8fb940c"
/> | <img width="1099" height="323" alt="preview-new-heaviour"
src="https://github.com/user-attachments/assets/b4a1e703-10d2-4378-9f25-48cac46249a1"
/> |
## Verification
Rendered each SessionTimeline story via a headless Chromium and asserted
whether "Session completed" is present:
| Story | `hasNextPage` | `isFetchingNextPage` | "Session completed" |
| --- | --- | --- | --- |
| OneThread | false | false | visible |
| MultipleThreads | false | false | visible |
| FetchingNextPage | true | true | hidden |
| HasMoreThreadsToLoad (new) | true | false | hidden |
All checks pass locally:
- `pnpm format` (no changes)
- `pnpm lint:check`
- `pnpm lint:types`
- `make pre-commit` via githooks
<details>
<summary>Implementation notes</summary>
-
`site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.tsx`:
wrap the row 7 spacer and row 8 status dot/text in `!hasNextPage &&
!isFetchingNextPage`.
- `SessionTimeline.stories.tsx`: add `HasMoreThreadsToLoad` to cover the
between-fetches state.
- No prop signature or public API change; `SessionTimelineSkeleton.tsx`
is untouched because the skeleton is only shown before any threads have
loaded.
</details>
Tasks created through the API now enforce required external auth:
`tasksCreate` rejects an owner who is missing a required (non-optional)
provider with a 403 before generating a task name or inserting any rows,
matching the gate `createWorkspace` already applies to workspaces. Adds
`TestCreateTaskExternalAuth` covering the required and optional-provider
cases.
Fixes PLAT-298.
_Coder Agents generated._
gpt-5.6 models were unusable with agents: fantasy's Responses allowlist
did not include the new family, so `IsResponsesModel` returned false and
chatd fell back to the Chat Completions path (no reasoning params, no
encrypted reasoning continuity).
## Changes
- Bump the `charm.land/fantasy` replace pin to coder/fantasy
`6da0c3b10237` (coder_2_33), pulling in:
- coder/fantasy#46: route `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`
through the OpenAI Responses API as reasoning models.
- coder/fantasy#41: surface Anthropic refusal stop_reason as
content-filter (already on coder_2_33, rides along with the bump).
- Update the fork changelog comment in go.mod.
## Verification
- Probed all three gpt-5.6 models through the ai-gateway:
`/v1/responses` with `reasoning.effort`, `include:
["reasoning.encrypted_content"]`, and `store: false` completes for each.
- `go build ./coderd/...` and `go test ./coderd/x/chatd/chatopenai/
./coderd/x/chatd/chatprovider/` pass against the new pin.
> This PR was authored by Mux on Mike's behalf.
closes DEVEX-588
Prototyped in #27077, broken off into a separate PR to make this work
easier to track
## changes
- Reveals the previously hidden trash can icon within
`ModuleConfiguration` (main content area)
- Removes the "x" icons from `ModuleSelection` (sidebar)
## context
@tracyjohnsonux and I decided [in
Slack](https://codercom.slack.com/archives/C0AUKB54P0E/p1783456607073329?thread_ts=1783450189.570979&cid=C0AUKB54P0E)
that it would be a better UX to move the deletion action from the "x"
icons in the sidebar to the trash can icons in the main content area.
This change has the benefits of
1. making it harder to delete modules accidentally
2. removing the responsibility of deletion from the items in
`ModuleSelection`
- interacting with these items will serve only to navigate to
configuring that module (DEVEX-587, to be done in a separate PR)
<img width="1840" height="1191" alt="image"
src="https://github.com/user-attachments/assets/4571ea2f-75cf-4cd7-b626-1826eea83bdf"
/>
Update release calendar with the latest branch releases:
- v2.34.1 → v2.34.5 (Stable/ESR)
- v2.33.7 → v2.33.11 (Security Support)
- v2.32.6 → v2.32.10 (Not Supported)
- v2.29.16 → v2.29.19 (Extended Support Release)
- 2.35 added as Mainline at v2.35.1
Channel rotation for the 2.35 mainline release:
- 2.32: Security Support → Not Supported
- 2.33: Stable → Security Support
- 2.34: Mainline (ESR) → Stable (ESR)
- 2.35: Not Released → Mainline
Also updates the ESR version link to point to v2.34.5.
Test_TaskSend flaked (coder/internal#1547, coder/internal#1609) when a
stray POST /chat/completions hit the fake agent API and the catch-all
handler called t.Fatalf. No code under test posts that path to the
sidebar app URL; the request most likely came from another test's
lingering client after its server's ephemeral port was reused. Fatalf
was also called off the test goroutine, which the testing package
forbids.
Unknown paths now get a 404 and a log line with request details for
attribution. Unstubbed known agentapi endpoints still fail the test, via
t.Errorf, so a coderd regression is still caught.
Previously, bulk start required every selected workspace to be stopped,
and bulk stop required every selected workspace to be running. Mixed
selections disabled both buttons entirely.
- Change the disabled checks on bulk start/stop from `every()` to
`some()` so the buttons are enabled when at least one workspace is
eligible.
- Filter workspaces by status in the mutation functions so only eligible
workspaces are sent to the API, matching the pattern used by other batch
mutations (update, favorite, unfavorite).
- Update docs to reflect the new behavior.
> [!NOTE]
> Generated by Coder Agents. [View session](https://coder.com/).
<details>
<summary>Implementation plan</summary>
## Problem
When an admin selects multiple workspaces and opens the "Bulk actions"
dropdown, the **Start** menu item is disabled unless *every* selected
workspace has `latest_build.status === "stopped"`. If even one workspace
is already running (or in any other non-stopped state), the Start button
is grayed out and unusable. Same issue applies to **Stop**.
## Changes
### 1. Relax disabled condition (`WorkspacesPageView.tsx`)
Changed `every()` to `some()` for both Start and Stop dropdown items.
The buttons are now enabled when at least one selected workspace is in
the target state.
### 2. Filter in mutations (`batchActions.ts`)
Added `.filter()` before `.map()` in both `startAllMutation` and
`stopAllMutation` so only eligible workspaces hit the API. This matches
the existing pattern in `updateAllMutation`, `favoriteAllMutation`, and
`unfavoriteAllMutation`.
### 3. Update documentation (`docs/user-guides/workspace-management.md`)
Replaced "can only be applied to a set of workspaces which are all in
the same state" with "apply to eligible workspaces in the selection,
skipping workspaces that are already in the target state."
## Testing
Four new Storybook stories:
| Story | What it tests |
|-------|---------------|
| `StartIgnoresAlreadyRunningWorkspaces` | Mixed selection; only stopped
workspaces get `startWorkspace` calls |
| `StopIgnoresAlreadyStoppedWorkspaces` | Mixed selection; only running
workspaces get `stopWorkspace` calls |
| `StartDisabledWhenNoWorkspacesAreStartable` | All running; Start
button is disabled |
| `StopDisabledWhenNoWorkspacesAreStoppable` | All stopped; Stop button
is disabled |
</details>
## Problem
`coder config-ssh --ssh-host-prefix=""` (or the matching env var,
`CODER_CONFIGSSH_SSH_HOST_PREFIX=`) was silently ignored, and the
deprecated `Host coder.*` block was written to the SSH config anyway.
The
merge logic that decides whether to fall back to the server's default
prefix checked `user.userHostPrefix == ""`, which is true both when the
flag was never passed and when it was explicitly set to empty, so there
was no way to distinguish the two. The same issue applied to
`--hostname-suffix`.
## How this affects users
Anyone who wants to opt out of the legacy prefix-based SSH aliases
(`ssh coder.myworkspace`) in favor of the newer suffix-based ones
(`ssh myworkspace.coder`) had no way to do so, the `Host coder.*`
wildcard
block kept reappearing on every `config-ssh` run regardless of the flag.
Because that wildcard matches any hostname starting with `coder.`, not
just Coder workspaces, it can silently intercept SSH connections to
unrelated hosts that happen to share that prefix.
It got worse on top of that: even after passing `--ssh-host-prefix=""`,
running `config-ssh --use-previous-options` in a later session, a normal
way to refresh local config without retyping every flag, would silently
bring the block back, because the empty choice was never persisted to
the
file in the first place.
## Solution
Track whether each option (`--ssh-host-prefix`, `--hostname-suffix`) was
explicitly set by the user, as opposed to left at its zero value, and
only
fall back to the server default (or skip persisting the option) when it
was genuinely never set.
## How it works
Two new fields on `sshConfigOptions`, `userHostPrefixExplicit` and
`hostnameSuffixExplicit`, carry this information:
- **Live invocation**: they're set from `userSetOption(inv, ...)`, which
inspects serpent's `Option.ValueSource` for the flag, right after
`header`/`headerCommand` are set in the `Handler`, before any
`--use-previous-options`/prompt logic can replace the struct wholesale
from a prior run's saved options.
- **Persistence**: `sshConfigWriteSectionHeader` now writes the
`# :ssh-host-prefix=` comment line even when the value is empty, as long
as it was explicit, and `sshConfigParseLastOptions` sets the field back
to `true` whenever it parses that line on a later run, regardless of
value.
`mergeSSHOptions`'s fallback condition changed from
`user.userHostPrefix == ""` to
`user.userHostPrefix == "" && !user.userHostPrefixExplicit` (and the
mirror for suffix). `equal()` and `asList()` were extended to include
the
two new fields so the `--dry-run` diff and "options differ, use new
ones?"
prompt stay accurate.
## Why implemented this way
- Reuses `userSetOption` (`cli/util.go`), an existing helper already
used
for this exact "distinguish zero value from unset" problem elsewhere in
the CLI (`cli/templateedit.go`), instead of inventing new machinery.
- Storing the "explicit" bit as a plain field on `sshConfigOptions`,
rather
than as extra parameters to `mergeSSHOptions`, keeps that function
dependency-free (still plain data in, plain data out, no
`serpent.Invocation` coupling), while letting the same bit flow
naturally
through the SSH config's persisted-options comment, solving the
live-flag
case and the `--use-previous-options` persistence case with one
mechanism instead of two.
- A sentinel-value approach was considered and rejected: a self-tracking
custom `serpent.Value` doesn't work because serpent applies a flag's
default through the same `Value.Set()` call used for real input, so it
can't tell the two apart; a plain sentinel string would work but leak
into several other code paths (equality checks, diff/prompt text, the
persisted comment) that would all need to filter it out.
Closes https://github.com/coder/internal/issues/1208
## Manual verification
Every step below was run against a local dev server
(`./scripts/develop.sh`
+ `./scripts/coder-dev.sh`), pointed at a throwaway `--ssh-config-file`,
never a real `~/.ssh/config`.
### 1. Baseline: unchanged behavior with no flags
```sh
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG"
cat "$TEST_SSH_CONFIG"
```
Both `Host coder.*` and `Host *.coder` are written, unchanged from
before this fix (both server defaults are non-empty out of the box).
<details>
<summary>Output</summary>
```text
Updated "/tmp/tmp.9Y7VIeuQoY"
You should now be able to ssh into your workspace.
For example, try running:
$ ssh myworkspace.coder
# ------------START-CODER-----------
# This section is managed by coder. DO NOT EDIT.
#
# You should not hand-edit this section unless you are removing it, all
# changes will be lost when running "coder config-ssh".
#
Host coder.*
ConnectTimeout=0
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
LogLevel ERROR
ProxyCommand .../coder-slim ... ssh --stdio --ssh-host-prefix coder. %h
Host *.coder
ConnectTimeout=0
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
LogLevel ERROR
Match host *.coder !exec ".../coder-slim connect exists %h"
ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h
# ------------END-CODER------------
```
</details>
### 2. Explicit empty `--ssh-host-prefix` omits the legacy block (the
core fix)
```sh
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix ""
cat "$TEST_SSH_CONFIG"
```
`Host coder.*` is gone, only `Host *.coder` remains. The choice is now
also persisted (`# :ssh-host-prefix=`).
<details>
<summary>Output</summary>
```text
Updated "/tmp/tmp.9Y7VIeuQoY"
You should now be able to ssh into your workspace.
For example, try running:
$ ssh myworkspace.coder
# ------------START-CODER-----------
# This section is managed by coder. DO NOT EDIT.
#
# You should not hand-edit this section unless you are removing it, all
# changes will be lost when running "coder config-ssh".
#
# Last config-ssh options:
# :ssh-host-prefix=
#
Host *.coder
ConnectTimeout=0
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
LogLevel ERROR
Match host *.coder !exec ".../coder-slim connect exists %h"
ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h
# ------------END-CODER------------
```
</details>
### 3. Same, via the environment variable instead of the flag
```sh
CODER_CONFIGSSH_SSH_HOST_PREFIX="" ./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG"
grep -c "Host coder" "$TEST_SSH_CONFIG"
```
Confirms the fix isn't flag-only, `userSetOption` checks `ValueSource`,
set the same way for `ValueSourceFlag` and `ValueSourceEnv`.
<details>
<summary>Output</summary>
```text
Updated "/tmp/tmp.9Y7VIeuQoY"
You should now be able to ssh into your workspace.
For example, try running:
$ ssh myworkspace.coder
0
```
</details>
### 4. Explicit empty prefix combined with an explicit suffix
```sh
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "" --hostname-suffix mytest
cat "$TEST_SSH_CONFIG"
```
Only `Host *.mytest` is written. Both options are correctly recorded in
the persisted comment.
<details>
<summary>Output</summary>
```text
Updated "/tmp/tmp.9Y7VIeuQoY"
You should now be able to ssh into your workspace.
For example, try running:
$ ssh myworkspace.mytest
# ------------START-CODER-----------
# This section is managed by coder. DO NOT EDIT.
#
# You should not hand-edit this section unless you are removing it, all
# changes will be lost when running "coder config-ssh".
#
# Last config-ssh options:
# :ssh-host-prefix=
# :hostname-suffix=mytest
#
Host *.mytest
ConnectTimeout=0
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
LogLevel ERROR
Match host *.mytest !exec ".../coder-slim connect exists %h"
ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix mytest %h
# ------------END-CODER------------
```
</details>
### 5. The explicitly-empty choice survives `--use-previous-options`
with no flag repeated
This is the persistence half of the fix: confirms the "omit this block"
choice, once persisted, doesn't get lost on a later run that reuses
previous options without repeating `--ssh-host-prefix`. Before this fix,
this exact sequence would bring `Host coder.*` back.
```sh
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix ""
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --use-previous-options
cat "$TEST_SSH_CONFIG"
```
<details>
<summary>Output</summary>
```text
Updated "/tmp/tmp.9Y7VIeuQoY"
You should now be able to ssh into your workspace.
For example, try running:
$ ssh myworkspace.coder
No changes to make.
# ------------START-CODER-----------
# This section is managed by coder. DO NOT EDIT.
#
# You should not hand-edit this section unless you are removing it, all
# changes will be lost when running "coder config-ssh".
#
# Last config-ssh options:
# :ssh-host-prefix=
#
Host *.coder
ConnectTimeout=0
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
LogLevel ERROR
Match host *.coder !exec ".../coder-slim connect exists %h"
ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h
# ------------END-CODER------------
```
</details>
The second command printed `No changes to make.`, and critically, `Host
coder.*` did **not** reappear even though that run passed no
`--ssh-host-prefix` flag at all, only `--use-previous-options`.
### 6. `--use-previous-options` still wins over this run's explicit
empty flag (unaffected by this fix)
Confirms this fix didn't change the pre-existing, intentional precedence
of `--use-previous-options`: a previously-saved *non-empty* value still
wins over an explicit empty flag passed on a later run.
```sh
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "custom-test."
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --use-previous-options --ssh-host-prefix ""
cat "$TEST_SSH_CONFIG"
```
<details>
<summary>Output</summary>
```text
Updated "/tmp/tmp.9Y7VIeuQoY"
You should now be able to ssh into your workspace.
For example, try running:
$ ssh myworkspace.coder
No changes to make.
# ------------START-CODER-----------
# This section is managed by coder. DO NOT EDIT.
#
# You should not hand-edit this section unless you are removing it, all
# changes will be lost when running "coder config-ssh".
#
# Last config-ssh options:
# :ssh-host-prefix=custom-test.
#
Host custom-test.*
ConnectTimeout=0
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
LogLevel ERROR
ProxyCommand .../coder-slim ... ssh --stdio --ssh-host-prefix custom-test. %h
Host *.coder
ConnectTimeout=0
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
LogLevel ERROR
Match host *.coder !exec ".../coder-slim connect exists %h"
ProxyCommand .../coder-slim ... ssh --stdio --hostname-suffix coder %h
# ------------END-CODER------------
```
</details>
`Host custom-test.*` is preserved verbatim, `--use-previous-options`
correctly overrides the explicit empty flag when the saved value is
non-empty, the mirror image of step 5's explicit-empty saved value.
### 7. End-to-end sanity check with a real workspace
```sh
./scripts/coder-dev.sh config-ssh --yes --ssh-config-file "$TEST_SSH_CONFIG" --ssh-host-prefix "" --hostname-suffix mytest
ssh -F "$TEST_SSH_CONFIG" -o ConnectTimeout=15 myworkspace.mytest echo ok
```
<details>
<summary>Output</summary>
```text
Updated "/tmp/tmp.9Y7VIeuQoY"
You should now be able to ssh into your workspace.
For example, try running:
$ ssh myworkspace.mytest
ok
```
</details>
`ok` came back from a real, running workspace, confirming the
ProxyCommand and Match/exec wiring generated by the suffix-only config
actually establishes a working SSH session end-to-end, not just a
text-generation check.
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
The UI guard added in #22112 disabled the `Activity bump` field and
cleared its saved value whenever the template's `Default autostop` was
0. It did not check the "Allow users to customize autostop duration for
workspaces" (`allow_user_autostop`) setting, so templates that relied on
user-defined autostop timers had their `activity_bump_ms` silently
cleared when saving in the Coder UI.
Enable the field, preserve the value on submit, and update the helper
text when either `default_ttl_ms > 0` or `allow_user_autostop` is true.
Closes
[DEVEX-438](https://linear.app/codercom/issue/DEVEX-438/allow-user-autostop-default-autostop-disabled-causes-activity-bump-to).
> **Note:** This needs to be backported to 2.34 (ESR).
<details>
<summary>Implementation notes</summary>
### Problem
[#22112](https://github.com/coder/coder/pull/22112) introduced a UI
guard that:
1. Disables the `Activity bump (hours)` field when `default_ttl_ms ===
0`.
2. Sends `activity_bump_ms: undefined` on submit under the same
condition, which the backend treats as "do not update", but combined
with the disabled state users cannot re-enter a value once cleared and
the previously stored value effectively becomes orphaned.
The guard ignored `allow_user_autostop`. When that setting is enabled,
workspaces still have a scheduled stop (whatever the user configures on
their workspace), so `activity_bump_ms` is still meaningful.
### Fix
Broaden the guard to consider both signals. The field is only disabled
and the value only discarded when **both** `default_ttl_ms === 0`
**and** `allow_user_autostop === false`.
Changes:
- `TemplateScheduleForm.tsx`
- `disabled` prop now checks `!default_ttl_ms && !allow_user_autostop`.
- Submit path preserves `activity_bump_ms` when either signal is truthy.
- Passes `allowUserAutostop` through to the helper text.
- `TTLHelperText.tsx`
- `ActivityBumpHelperText` accepts `allowUserAutostop` and only shows
the "no scheduled stop" hint when neither signal is set. Updated copy
mentions both signals.
- Tests and stories
- Existing tests explicitly uncheck `allow_user_autostop` before
asserting the guard fires (since `MockTemplate.allow_user_autostop`
defaults to `true`).
- Added coverage: guard stays off when only `allow_user_autostop` is
enabled; toggling `allow_user_autostop` re-enables the field without
touching `default_ttl_ms`.
- Added a story that verifies `activity_bump_ms` is preserved on submit
when `allow_user_autostop` is enabled and `default_ttl_ms` is 0.
</details>
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://github.com/coder/internal/issues/1615. The affected test
was starting coderd with a live chatd worker, but assumed that the chat
would not be processed by a worker. The fix was to start coderd without
a chatd worker. I noticed that some other tests in the file could suffer
from the same flake root cause, so I fixed them too.
Under CI load the request's 10ms revoke timeout could expire before
the request reached the FakeIDP revoke handler. The handler never
ran, so the test's wait for it to finish blocked until the 25s test
context expired instead of passing quickly.
Raise `RevokeTimeout` to 100ms so the request has ~10x more headroom to
reach the handler under load. After RevokeToken returns, check a
`handlerStarted` signal before asserting: this anchors the
`DeadlineExceeded` assertion to a request that was actually in flight,
and turns any residual scheduling race into a fast, labeled failure
instead of a hang.
Unblock the handler on the early-exit path with a `t.Cleanup`. It must
be registered after the FakeIDP setup so LIFO runs it before the
server's `Close()`; otherwise a handler that dispatched late would
block `Close()` and hang teardown until the test timeout. Drop the
previous `time.Sleep` watchdog and the handler-done channel, since the
FakeIDP server's `Close()` already joins the in-flight handler.
Refs: https://linear.app/codercom/issue/PLAT-317
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.
Child chats (sub-agent chats) no longer offer archive-state actions in
their menus. Archive state is root-only on the backend and cascades to
children (`coderd/exp_chats.go` rejects `archived` changes when
`parent_chat_id` is set), so a child's "Archive agent", "Archive &
delete workspace", and "Unarchive agent" items always failed with a 400.
All chat action menus (chat header kebab, sidebar row dropdown, sidebar
right-click context menu) render the shared `ChatActionsMenuItems`,
which already hides Pin/Unpin for child chats; this extends the same
gating to the archive and unarchive items.
Since an archived child chat then has no menu actions at all, the menu
triggers are hidden for archived child chats (`chatHasMenuActions`): the
header kebab and the sidebar row's dropdown trigger are not rendered,
and the row's right-click context menu is disabled. Archived root chats
keep their "Unarchive agent" action.
Stories: renamed the ChatTopBar child-chat story to
`ChildChatHidesPinAndArchiveActions` and extended it to assert both
archive items are hidden, plus new stories for the archived-child cases
(`ArchivedChildChatHasNoActionsMenu`,
`ArchivedChildChatRowHasNoActionsMenu`) and a sidebar child-menu story
(`ChildChatMenuHidesArchiveActions`).
Closes CODAGT-631.
> This PR was created by Mux, an AI agent working on behalf of Mike.
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.