related to: https://github.com/coder/internal/issues/1387
Sometimes our tests using DERP flake like the above, but we have no information at the DERP layer about why. This enables verbose DERP logging in CI.
Logs are kinda spammy, but most tests do not force DERP and so only use it for discovery (disco).
A test like TestWorkspaceAgentListeningPorts/OK_BlockDirect drops around 50 DERP packets e.g.
```
t.go:111: 2026-03-13 15:20:52.201 [debu] agent.net.tailnet.net.wgengine: magicsock: got derp-999 packet: "\x04\x00\x00\x00C}\xe0\xb9\x00\x00\x00\x00\x00\x00\x00\x00}\xae;l\xd9Hk8\xa8l\x1cK\xedO{狦)\x18NIw\xc4k\xd2-\x19\xbf\xfb\xdd\x17\xa9b\xac\xfd#\xf7\xcaC\xbe\vq(u=\xa7\x16\xe9\x9aLjS\x1fXL\x19y\xf4\x1dE%\xb3\xff\x9d:8\xa9\x95X\xfe\xf8\x95\x7f\x9dv$\f\xf4\xbe"
t.go:111: 2026-03-13 15:20:52.201 [debu] agent.net.tailnet.net.wgengine: magicsock: got derp-999 packet: "\x04\x00\x00\x00C}\xe0\xb9\x01\x00\x00\x00\x00\x00\x00\x00\x05x\xb6RD;\xb4\x80\xb6\x0f\xf6KŠc\xfb1\xbd\x06\xb70K3\x97`\x8d\xd2\x14\xed\xc5\xd6\xc6\xcaV\xbf\x878\xb2Ƥ\xf0\xd5\xf7\xc0\x1b\x9f\x04Y\x03\x17\xd4\x06\xee\xb2G\r){\x9f\xde\xe0(\xb5N\xfejR_\xf6q\xa4\xfaT\x9a\xd8\xcbk\xba\x16K"
t.go:111: 2026-03-13 15:20:52.201 [debu] agent.net.tailnet.net.wgengine: magicsock: got derp-999 packet: "\x04\x00\x00\x00C}\xe0\xb9\x02\x00\x00\x00\x00\x00\x00\x00\x1e\x93a\x15\xfev\x81'\xa9?\xe8nR\xce<\x91\x86\xcau@\xb9\xcfɩ\xef\xd1眓\x95\xf3*X^7\x99\x88\xb0|\x8cS\xe4@[\x16\xda\xca\xd4\xd9\x1dP\xd0\xfe\xd9r\x8c\xfcp~dP\xfaK\xe0\xf9y\xb2\x11\x15\xfe\xdcx鷽\xdeF\xf7\x92\xe8"
```
Migration 000434 converts chat_messages.role from text to a Postgres
enum, rebuilds the partial index, and adds content_version smallint.
The column is backfilled with DEFAULT 0, then the default is dropped
so future inserts must set it explicitly.
Version 0 uses the role-aware heuristic from #22958. Version 1 (all
new inserts) stores []ChatMessagePart JSON for all roles, including
system messages. ParseContent takes database.ChatMessage directly
and dispatches on version internally. Unknown versions error.
All string(codersdk.ChatMessageRole*) casts at DB write sites are
replaced with database.ChatMessageRole* constants from sqlc.
Refs #22958
File-reference parts in user messages were flattened to `TextContent` at
write time because fantasy has no file-reference content type. The
frontend never saw them as structured parts.
This moves all write paths (user, assistant, tool) from fantasy envelope
format to `codersdk.ChatMessagePart`. The streaming layer (`chatloop`)
is untouched, the conversion happens at the serialization boundary in
`persistStep`.
Old rows are still readable. `ParseContent` uses a structural heuristic
(`isFantasyEnvelopeFormat`) to distinguish legacy envelopes from SDK
parts. We chose this over try/fallback because fantasy envelopes
partially unmarshal into `ChatMessagePart` (the `type` field matches)
while silently losing content. A guard test enforces that no SDK part
can produce the envelope shape.
This is forward-only: new rows are unreadable by old code. Chat is
behind a feature flag so rollback risk is contained.
Also adds a typed `ChatMessageRole` to replace raw strings and
`fantasy.MessageRole*` casts at the persistence boundary. The type
covers `ChatMessage.Role`, `ChatStreamMessagePart.Role`, the
`PublishMessagePart` callback chain, and all DB write sites.
`fantasy.MessageRole*` remains only where we build `fantasy.Message`
structs for LLM dispatch.
Separately, `ProviderMetadata` was leaking to SSE clients via
`publishMessagePart`. `StripInternal` now runs on both the SSE and REST
paths, covering this.
Other cleanup:
- Old `db2sdk.contentBlockToPart` silently dropped metadata on
text/reasoning/tool-call content. New code preserves it.
- `providerMetadataToOptions` now logs warnings instead of silently
returning nil.
- `db2sdk` shrinks from ~250 lines of parallel conversion to ~15 lines
delegating to `chatprompt.ParseContent()`, removing the `fantasy` import
entirely.
Refs #22821
_Disclaimer: implemented by a Coder Agent using Claude Opus 4.6._
Marks the injected MCP approach in AI Bridge as deprecated across the
codebase.
## Changes
- **`codersdk/deployment.go`**: Deprecated `ExternalAuthConfig.MCPURL`,
`.MCPToolAllowRegex`, `.MCPToolDenyRegex` fields; deprecated and hid the
`--aibridge-inject-coder-mcp-tools` server flag; deprecated
`AIBridgeConfig.InjectCoderMCPTools`.
- **`coderd/externalauth/externalauth.go`**: Deprecated `Config.MCPURL`,
`.MCPToolAllowRegex`, `.MCPToolDenyRegex`.
- **`enterprise/aibridgedserver/aibridgedserver.go`**: Added runtime
deprecation warning when `CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS` is
enabled; deprecated `getCoderMCPServerConfig`.
- **`enterprise/aibridged/mcp.go`**: Deprecated `MCPProxyBuilder`
interface and `MCPProxyFactory` struct.
- **`docs/ai-coder/ai-bridge/mcp.md`**: Added deprecation warning
banner.
## Summary
Adds a new `GET /api/v2/debug/profile` endpoint that collects multiple
pprof profiles in a single request and returns them as a tar.gz archive.
This allows collecting profiles (including block and mutex) without
requiring `CODER_PPROF_ENABLE` to be set, and without restarting
`coderd`.
Closes#21679
## What it does
The endpoint:
- Temporarily enables block and mutex profiling (normally disabled at
runtime)
- Runs CPU profile and/or trace for a configurable duration (default
10s, max 60s)
- Collects snapshot profiles (heap, allocs, block, mutex, goroutine,
threadcreate)
- Returns a tar.gz archive containing all requested `.prof` files
- Uses an atomic bool to prevent concurrent collections (returns 409
Conflict)
- Is protected by the existing debug endpoint RBAC (owner-only)
**Supported profile types:** cpu, heap, allocs, block, mutex, goroutine,
threadcreate, trace
**Query parameters:**
- `duration`: How long to run timed profiles (default: `10s`, max:
`60s`)
- `profiles`: Comma-separated list of profile types (default:
`cpu,heap,allocs,block,mutex,goroutine`)
## Additional changes
- **SDK client method** (`codersdk.Client.DebugCollectProfile`) for easy
programmatic access
- **`coder support bundle --pprof` integration**: tries the consolidated
endpoint first, falls back to individual `/debug/pprof/*` endpoints for
older servers
- **8 new tests** covering defaults, custom profiles, trace+CPU,
validation errors, authorization, and conflict detection
Docker keeps named volumes around, even if they're dangling. During
development users generate a lot of named coder- volumes and the like.
These are now cleaned on shutdown once they are 30 days old.
For long running workspaces, go build cache also accumulates, so now
we clean these up via cron as well. All cache older than 2 days is wiped
and the cron schedule is based on workspace ID to distribute the load.
## Summary
Moves the messages response out of `GET /chats/{id}` and into a
dedicated `GET /chats/{id}/messages` endpoint.
### Backend
- `GET /chats/{id}` now returns just the `Chat` object (no messages)
- `GET /chats/{id}/messages` is a new endpoint returning
`ChatMessagesResponse` with `messages` and `queued_messages`
- Added `ChatMessagesResponse` SDK type and `GetChatMessages` client
method
### Frontend
- `getChat()` API method returns `Chat` instead of `ChatWithMessages`
- Added `getChatMessages()` API method for the new endpoint
- Split `chatQuery` into two: `chatQuery` (metadata) and
`chatMessagesQuery` (messages)
- Updated all cache mutations, optimistic updates, and websocket
handlers
- Updated tests and stories
### Files changed
| File | Change |
|---|---|
| `coderd/coderd.go` | Register `GET /messages` route |
| `coderd/chats.go` | Simplify `getChat`, add `getChatMessages` handler
|
| `codersdk/chats.go` | New type + method, update `GetChat` return |
| `site/src/api/api.ts` | New method, update `getChat` |
| `site/src/api/queries/chats.ts` | New query, update cache mutations |
| `site/src/pages/AgentsPage/AgentDetail.tsx` | Use separate queries |
| `site/src/pages/AgentsPage/AgentDetail/ChatContext.ts` | Update types
and cache writes |
| `site/src/pages/AgentsPage/AgentsPage.tsx` | Update websocket cache
handler |
## Summary
Adds a microphone button to the agent chat input for browser-native
voice-to-text transcription using the Web Speech API.
## Changes
### New: `site/src/hooks/useSpeechRecognition.ts`
- Custom React hook wrapping the Web Speech API (`SpeechRecognition` /
`webkitSpeechRecognition`)
- Feature-detects browser support via `isSupported`
- Provides `start()`, `stop()`, and `cancel()` controls
- Accumulates real-time transcript from interim and final recognition
results
- Inline TypeScript declarations for the Web Speech API types
### Modified: `site/src/pages/AgentsPage/AgentChatInput.tsx`
- **Mic button**: Appears to the right of the image attach button when
the browser supports the Speech Recognition API. Shows a microphone icon
when idle, X icon when recording.
- **Send button**: Transforms into a checkmark during recording to
accept the transcription. Always enabled during recording.
- **Editor sync**: Live-updates the Lexical editor with the
transcription as the user speaks. Preserves any pre-existing text.
- **Cancel**: Restores the editor to its pre-recording content.
## How it works
1. User clicks the mic button → recording starts, real-time transcript
appears in the editor
2. User clicks the checkmark (send button) → recording stops,
transcribed text stays
3. User clicks X (mic button) → recording stops, editor reverts to
original content
The timeout was started before the unbounded Stepper loop, so
under CI load the deadline could expire before reaching the
operations that actually use it.
Also bumps TestMigration000387 from WaitLong to WaitSuperLong.
Fixescoder/internal#1398
## Summary
Extract a `healthyChecker()` test helper that returns an all-healthy
baseline `testChecker` in `coderd/healthcheck`. Each `TestHealthcheck`
table-driven test case now only overrides the single report field being
tested, instead of repeating all 6 healthy report structs.
- Reduces `healthcheck_test.go` from 603 to 341 lines (~260 lines, 43%
reduction)
- Test coverage unchanged at 77.2%
- All test cases and assertions preserved exactly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- `coderd/httpapi/websocket.go`: add `net.ErrClosed` +
`websocket.CloseStatus` checks; extract `heartbeatCloseWith` with
`quartz.Clock` parameter for testability
- `coderd/httpapi/websocket_internal_test.go`: new test file
## Problem
When a chat is interrupted while tools are executing, the step content
(text, reasoning, tool calls, and partial tool results) was being lost.
Two gaps existed:
1. **During tool execution**: `executeTools` returns with error results
for interrupted tools, but the subsequent `PersistStep(ctx, ...)` fails
on the canceled context and returns `ErrInterrupted` without persisting
anything.
2. **PersistStep race**: If the context is canceled between the
post-tool interrupt check and the `PersistStep` call, the same loss
occurs.
This is inconsistent with how we handle stream interruptions (which
properly flush and persist partial content via `persistInterruptedStep`)
and how [coder/blink](https://github.com/coder/blink) handles
interruptions (always inserting the response message regardless of
execution phase).
## Fix
Two changes in `chatloop.go`:
- **Post-tool-execution interrupt check**: After `executeTools` returns,
check if the context was interrupted and route through
`persistInterruptedStep` (which uses `context.WithoutCancel` internally)
to save the accumulated content.
- **PersistStep fallback**: If `PersistStep` returns `ErrInterrupted`,
retry via `persistInterruptedStep` so partial content is not lost.
## Tests
- `TestRun_InterruptedDuringToolExecutionPersistsStep`: Verifies that
when a tool is blocked and the chat is interrupted, the step (text +
reasoning + tool call + tool error result) is persisted via the
interrupt-safe path.
- `TestRun_PersistStepInterruptedFallback`: Verifies that when
`PersistStep` itself returns `ErrInterrupted`, the step is retried via
the fallback path and content is saved.
## Problem
When a step contains both provider-executed tool calls (e.g. Anthropic
web search) and local tool calls in parallel, the next loop iteration
fails with the Anthropic API claiming the regular tool call has no
result. However, sending a new user message (which reloads messages from
the DB) works fine.
## Root cause
`toResponseMessages` was placing **all** tool results into the tool-role
message, regardless of `ProviderExecuted`. When Fantasy's Anthropic
provider later converted these messages for the API, it moved the
provider tool result from the tool message to the **end** of the
previous assistant message (`prevMsg.Content = append(...)`). This
placed `web_search_tool_result` **after** the regular `tool_use` block:
```
assistant: [server_tool_use(A), tool_use(B), web_search_tool_result(A)] ← wrong order
user: [tool_result(B)]
```
The persistence layer in `chatd.go` already handles this correctly —
provider-executed tool results stay in the assistant message, producing
the expected ordering:
```
assistant: [server_tool_use(A), web_search_tool_result(A), tool_use(B)] ← correct order
user: [tool_result(B)]
```
This is why reloading from the DB fixed it.
## Fix
In the `ContentTypeToolResult` case of `toResponseMessages`, route
provider-executed results to `assistantParts` instead of `toolParts`,
matching the persistence layer's behavior.
## Testing
Added
`TestToResponseMessages_ProviderExecutedToolResultInAssistantMessage`
which verifies that mixed provider+local tool results are split
correctly between the assistant and tool messages.
OverrideVSCodeConfigs previously unconditionally set
`git.useIntegratedAskPass` and `github.gitAuthentication` to false,
clobbering any values provided by template authors via module settings
(e.g. the vscode-web module's settings block). This change only set
these keys when they are not already present, so template-provided
values are preserved.
Registry PR [#758](https://github.com/coder/registry/pull/758) fixed the
module side (run.sh merges template-author settings into the existing
settings.json instead of overwriting the file). But the agent still
unconditionally stamped false onto both keys before the script ran, so
the merge base always contained the agent's values and template authors
couldn't set them to anything else. This change fixes the agent side by
only writing defaults when the keys are absent.
## Summary
When hovering on an agent row that is **not running**, the
expand/collapse chevron (`>`) for subagents was appearing by replacing
the status icon. This was visually distracting when scanning the
sidebar.
## Change
The chevron now only appears when hovering directly over the **icon
area** (`group-hover/icon`), not the entire row (`group-hover`). This
was already the behavior for running agents — this PR makes it
consistent for all states.
- Unified both the status-icon-hide and chevron-show hover triggers to
use `group-hover/icon`
- Removed the now-unused `isExecuting` variable (net -15 lines)
## Problem
LLMs stream list markers and item content as separate text blocks:
```json
{ "type": "text", "text": "Intro\n\n- " }
{ "type": "text", "text": "First item" }
{ "type": "text", "text": "\n- " }
{ "type": "text", "text": "Second item" }
```
The **streaming path** concatenated these directly → `"- First item"` ✅
The **completed path** used `appendText` which inserted `\n` between
chunks → `"- \nFirst item"` ❌
Every CommonMark parser treats `"- \nText"` (marker and content on
different lines, content not indented) as an empty `<li>` followed by a
sibling `<p>`, producing broken list rendering once a message finished
streaming.
## Fix
Make `appendText` use direct concatenation — the same as the streaming
path. The API text blocks already contain all necessary whitespace and
newlines; inserting extra `\n` between them was the bug.
## Changes
- **`messageParsing.ts`** — `appendText` simplified to direct concat
(skip whitespace-only chunks). `appendParsedTextBlock` no longer passes
a custom joiner, so it uses the same default as the streaming path.
- **`messageParsing.test.ts`** — Updated existing merge test
expectation; added regression test with the exact LLM list-marker
payload.
## Problem
The gitsync worker polls every 10s and refreshes up to 50 stale
`chat_diff_status` rows **sequentially**, sharing a single 10-second
context timeout. With 50 rows × 1–3 HTTP calls each, the timeout is
exhausted quickly, causing cascading `context deadline exceeded` errors.
Rows with no linked OAuth token (`ErrNoTokenAvailable`) fail fast but
recur every 120s, wasting batch capacity.
## Solution
Three targeted fixes:
### 1. Concurrent refresh processing
`Refresher.Refresh()` now launches goroutines bounded by a semaphore
(`defaultConcurrency = 10`). Provider/token resolution remains
sequential (fast DB lookups); only the HTTP calls run in parallel.
Per-group rate-limit detection uses `atomic.Pointer[RateLimitError]`
with best-effort skip of remaining rows — a rate-limit hit on one
provider doesn't stall requests to other providers.
### 2. Decoupled tick timeout
New `defaultTickTimeout = 30s`, separate from `defaultInterval = 10s`.
The `tick()` method uses `tickTimeout` for its context deadline, giving
concurrent HTTP calls enough headroom to complete without stalling the
next polling cycle.
### 3. Longer backoff for no-token errors
New `NoTokenBackoff = 10 * time.Minute` (exported). When `errors.Is(err,
ErrNoTokenAvailable)`, the worker applies a 10-minute backoff instead of
`DiffStatusTTL` (2 minutes). Retrying every 2 minutes is pointless until
the user manually links their external auth account.
## Design decisions
- Both `NewRefresher` and `NewWorker` accept variadic option functions
(`RefresherOption`, `WorkerOption`) for backward compatibility —
existing callers in `coderd/coderd.go` need no changes.
- `WithConcurrency(n)` and `WithTickTimeout(d)` are available for tests
and future tuning.
- Added `resolvedGroup` struct to cleanly separate the pre-resolution
phase from the concurrent execution phase.
## Testing
- **`TestRefresher_RateLimitSkipsRemainingInGroup`** — rewritten to be
goroutine-order-independent (verifies aggregate counts instead of
per-index results).
- **`TestRefresher_ConcurrentProcessing`** — new test using a gate
channel to prove N goroutines enter `FetchPullRequestStatus`
simultaneously.
- **`TestWorker_RefresherError_BacksOffRow`** — rewritten to use
branch-name-based failure determination instead of non-deterministic
`callCount`.
- **`TestWorker_NoTokenBackoff`** — new test verifying
`ErrNoTokenAvailable` triggers 10-minute backoff.
- All tests pass under `-race -count=3`.
## Problem
Both `start_workspace` and `create_workspace` chattool tools failed to
handle soft-deleted workspaces correctly.
Coder uses soft-delete for workspaces (`deleted = true` on the row).
Both tools called `GetWorkspaceByID`, which queries
`workspaces_expanded` with **no** `deleted = false` filter — so it
returns the workspace row even when soft-deleted. The only deletion
check was for `sql.ErrNoRows`, which never fires because the row still
exists.
### `start_workspace` behavior (before fix)
1. Loads the soft-deleted workspace successfully
2. Finds the latest build (a delete transition)
3. Falls through to attempt to **start** the deleted workspace
4. Produces a confusing downstream error
### `create_workspace` behavior (before fix)
1. `checkExistingWorkspace` loads the soft-deleted workspace
2. If a delete build is **in-progress**: waits for it, then falsely
reports `already_exists` — blocks new workspace creation
3. If the delete build **succeeded**: accidentally allows creation
(because no agents are found), but via fragile logic rather than an
explicit check
## Fix
Add `ws.Deleted` checks immediately after `GetWorkspaceByID` succeeds in
both tools:
- **`startworkspace.go`**: Returns `"workspace was deleted; use
create_workspace to make a new one"`
- **`createworkspace.go`** (`checkExistingWorkspace`): Returns `(nil,
false, nil)` to allow new workspace creation
## Tests
- `TestStartWorkspace/DeletedWorkspace` — verifies `start_workspace`
returns deleted error and never calls `StartFn`
- `TestCheckExistingWorkspace_DeletedWorkspace` — verifies
`checkExistingWorkspace` allows creation for soft-deleted workspaces
WaitBuffer is a thread-safe io.Writer that supports blocking until
accumulated output matches a substring or custom predicate. It
replaces ad-hoc safeBuffer/syncWriter types and time.Sleep-based
poll loops in tests with signal-driven waits.
- WaitFor/WaitForNth/WaitForCond for blocking on output
- Replace custom buffer types in cli/sync_test.go and
provisionersdk/agent_test.go
- Convert time.Sleep poll loops to require.Eventually/require.Never
in cli/ssh_test.go, coderd/activitybump_test.go,
coderd/workspaceagentsrpc_test.go, workspaceproxy_test.go, and
scaletest tests
Fixes Anthropic 400 error on multi-turn conversations with web search:
> web_search tool use with id srvtoolu_... was found without a
corresponding web_search_tool_result block
Provider-executed tool results (e.g. `web_search`) had a nil `Result`
field, which serialized as `"result":null`. Fantasy's
`UnmarshalToolResultOutputContent` couldn't deserialize `null` back, so
the entire assistant message became unreadable after persistence. On the
next LLM call, Anthropic rejected the conversation because
`server_tool_use` had no matching `web_search_tool_result`.
**Fix:** Bump the fantasy fork to e4bbc7bb3054 which returns `nil, nil`
for null `Result` JSON instead of erroring.
**Testing:** Added `integration_test.go` with
`TestAnthropicWebSearchRoundTrip` (requires `ANTHROPIC_API_KEY`) that:
- Sends a query triggering web search
- Verifies the persisted assistant message contains all parts the UI
needs: `tool-call(PE)`, `source`, `tool-result(PE)`, and `text`
- Sends a follow-up to confirm the round-trip works with Anthropic
When a PR introduces new files and docs link to them via
github.com/coder/coder/blob/main/..., linkspector returns 404
because the files don't exist on main yet.
Add a step that, for PR events only, appends replacementPatterns
to the linkspector config rewriting blob/main/ and tree/main/
URLs to use the PR's head commit SHA.
Refs #22922
When SSH disconnects, the output reader subshells fail writing to the
dead terminal (EIO) and exit due to set -e. This breaks the pipe to
the server, which receives SIGPIPE and dies before graceful shutdown
can stop embedded PostgreSQL.
Disable errexit in readers so they survive terminal death, add HUP
traps so the script handles SSH disconnect, and shield children from
direct SIGHUP via SIG_IGN before exec (Go resets this to caught once
signal.Notify registers). Also make fatal() exit immediately instead
of relying on async signal delivery, and remove the broken process
group kill (ppid was never a PGID).
## Problem
Anthropic's API returns a 400 error when `web_search` tool results are
missing:
```
web_search tool use with id srvtoolu_... was found without a corresponding web_search_tool_result block
```
**Root cause:** `persistStep` in `chatd.go` splits ALL
`ToolResultContent` blocks into separate tool-role DB rows.
Provider-executed (PE) tool results like `web_search` must stay in the
assistant message — Anthropic expects `server_tool_use` and
`web_search_tool_result` in the same turn.
The previous fix (#22976) added repair passes to drop PE results during
reconstruction, which fixed cross-step orphans but broke the normal case
(PE result correctly in the same step).
## Fix
Three changes that address the root cause:
1. **`persistStep` (chatd.go):** Check `ProviderExecuted` before
splitting `ToolResultContent` into tool rows. PE results stay in
`assistantBlocks` and are stored in the assistant content column.
2. **`ToMessageParts` (chatprompt.go):** Propagate the
`ProviderExecuted` field to `ToolResultPart` so the fantasy Anthropic
provider can identify PE results and reconstruct the
`web_search_tool_result` block.
3. **Keep existing repair passes** for backward compatibility with
legacy DB data where PE results were incorrectly persisted as separate
tool messages.
## Tests
- `TestProviderExecutedResultInAssistantContent` — PE result stored
inline in assistant content round-trips correctly with
`ProviderExecuted` preserved.
- `TestProviderExecutedResult_LegacyToolRow` — legacy PE results in
tool-role rows are still dropped correctly.
- All existing tests pass (including the 3 PE tests from #22976).
## Summary
- avoid duplicating preset headers when cachecompress serves compressed
`/bin/*` responses
- add a cachecompress regression test for preset
`X-Original-Content-Length` and `ETag` headers
- strengthen site binary tests to assert those headers stay
single-valued
## Problem
`site/bin.go` sets `X-Original-Content-Length` and `ETag` on the real
response writer before delegating.
`cachecompress` then snapshotted those headers and replayed them with
`Header().Add(...)`, which duplicated them on compressed responses.
For `coder-desktop-macos`, duplicate `X-Original-Content-Length` values
can collapse into a comma-separated string and fail `Int64` parsing,
causing the file size to show as `Unknown`.
## Testing
- `/usr/local/go/bin/go test ./coderd/cachecompress -run
'TestCompressorPresetHeaders|TestCompressorHeadings' -count=1`
- `/usr/local/go/bin/go test ./site -run TestServingBin -count=1`
- `PATH=/usr/local/go/bin:$PATH make lint/go`
## Notes
- Skipped full `make pre-commit` with explicit approval because local
environment/tooling blocked it (Node version/path interaction in
generated site targets, plus missing local tools before setup).
The `coder_app` resource no longer supports having both `command` and
`subdomain` set simultaneously. This removes `subdomain = true` from the
`develop_sh` app in dogfood, which uses `command`.
This was the only `coder_app` in the repo with both fields set.
## Problem
The summarization prompt explicitly tells the model to **"Omit
pleasantries and next-step suggestions"** and the summary prefix frames
the compacted context as passive history: `Summary of earlier chat
context:`. After compaction mid-task, the model reads a factual recap
with no forward momentum, loses its direction, and either stops or asks
the user what to do.
## Research
I compared our compaction prompt against several other agents:
| Agent | Key Pattern |
|---|---|
| **Codex** | Prompt says *"Include what remains to be done (clear next
steps)"*. Prefix: *"Another language model started to solve this
problem..."* |
| **Mux** | Includes *"Current state of the work (what's done, what's in
progress)"* + appends the user's follow-up intent |
| **Continue** | *"Make sure it is clear what the current stream of work
was at the very end prior to compaction so that you can continue exactly
where you left off"* |
| **Copilot Chat** | Dedicated sections for *Active Work State*, *Recent
Operations*, *Pre-Summary State*, and a *Continuation Plan* with
explicit next actions |
**Every other major agent explicitly preserves forward intent and
in-progress state.** Coder was the only one telling the model to omit
next steps.
## Changes
**Summary prompt:**
- Removes `Omit next-step suggestions`
- Adds structured `Include:` list with explicit items for in-progress
work, remaining work, and the specific action being performed when
compaction fired
- Frames the operation as `context compaction` (matching Codex's
framing)
**Summary prefix:**
- Old: `Summary of earlier chat context:`
- New: `The following is a summary of the earlier conversation. The
assistant was actively working when the context was compacted. Continue
the work described below:`
The prefix is the first thing the model reads post-compaction — framing
it as an active handoff with an explicit "Continue" directive primes the
model to resume work rather than wait.
The pre-push hook was removed in #22956. This restores it with a
reduced scope (tests + site build) and an allowlist so it only runs
for developers who opt in.
Two opt-in mechanisms:
- git config coder.pre-push true (local, not committed)
- CODER_WORKSPACE_OWNER_NAME allowlist in the hook script
git config takes priority and also supports explicit opt-out for
allowlisted users (git config coder.pre-push false).
Refs #22956
---------
Co-authored-by: Cian Johnston <cian@coder.com>
pre-commit was noisy: every sub-target dumped full stdout/stderr to the
terminal, burying failures in pages of compiler output and lint details.
Teach timed-shell.sh a quiet mode via MAKE_LOGDIR: when set, recipe
output is redirected to per-target log files and a one-line status is
printed instead. When unset, behavior is unchanged (with a refreshed
output format).
Makefile changes:
- pre-commit creates a tmpdir, passes MAKE_LOGDIR to sub-makes
- Drop --output-sync=target (log files eliminate interleaving)
- Add --no-print-directory to suppress Entering/Leaving noise
- Split check-unstaged and check-untracked into separate defines
- Restyle both with colored indicators and clearer instructions
- Clean up tmpdir on success, preserve on failure for debugging
_Generated with mux but reviewed by a human_
This PR fixes a bug where Coder Desktop could stop retrying connections
to coderd after a prolonged network interruption. When that happened,
the client would no longer recoordinate or receive workspace updates,
even after connectivity returned.
This is likely the long-standing “stale connection” issue that has been
reported both internally and by customers. In practice, it would cause
all Coder Desktop workspaces to appear yellow or red in the UI and
become unreachable.
The underlying behavior matches the reports: peers are removed after 15
minutes without a handshake. So if network connectivity is lost for that
long, the client must recoordinate to recover. This bug prevented that
recoordination from happening.
For that reason, I’m marking this as:
Closes https://github.com/coder/coder-desktop-macos/issues/227
## Problem
The tailnet controller owns a long-lived retry loop in `Controller.Run`.
That loop already had an important graceful-shutdown guard added in
[`ba21ba87`](https://github.com/coder/coder/commit/ba21ba87ba2209fad3c9f4bb131d7de1fc0e58be)
to prevent a phantom redial after cancellation:
```go
if c.ctx.Err() != nil {
return
}
```
That guard was correct. It made controller lifetime depend on the
controller's own context rather than on retry timing races.
But the post-dial error path had since grown a broader terminal check:
```go
if xerrors.Is(err, context.Canceled) ||
xerrors.Is(err, context.DeadlineExceeded) {
return
}
```
That turns out to be too broad for desktop reconnects. A dial attempt
can fail with a wrapped `context.DeadlineExceeded` even while the
controller's own context is still live.
## Why that happens
The workspace tailnet dialer uses the SDK HTTP client, which inherits
`http.DefaultTransport`. That transport uses a `net.Dialer` with a 30s
`Timeout`. Go implements that timeout by creating an internal
deadline-bound sub-context for the TCP connect.
So during a control-plane blackhole, the reconnect path can look like
this:
- the existing control-plane connection dies
- `Controller.Run` re-enters the retry path
- the next websocket/TCP dial hangs against unreachable coderd
- `net.Dialer` times out the connect after ~30s
- the returned error unwraps to `context.DeadlineExceeded`
- `Controller.Run` treats that as terminal and returns
- the retry goroutine exits forever even though `c.ctx` is still alive
At that point the data plane can remain partially alive, the desktop app
can still look online, and unblocking coderd does nothing because the
process is no longer trying to redial.
## How this was found
We reproduced the issue in the macOS vpn-daemon process with temporary
diagnostics, blackholed coderd with `pfctl`, and captured multiple
goroutine dumps while the daemon was wedged.
Those dumps showed:
- `manageGracefulTimeout` was still blocked on `<-c.ctx.Done()`, proving
the controller context was not canceled
- the `Controller.Run` retry goroutine was missing from later dumps
- control-plane consumers stayed idle longer over time
- once coderd became reachable again the daemon still did not dial it
That narrowed the failure from "slow retry" to "retry loop exited", and
tracing the dial path back through `http.DefaultTransport` and
`net.Dialer` explained why a transport timeout was being mistaken for
controller shutdown.
In my testing with coderd blocked, as expected, I did retain a
connection to the workspace agent. I suspect the scenarios where
connection to the agent are lost is because we can't retry coordination.
## Fix
Keep the graceful-shutdown guard from
[`ba21ba87`](https://github.com/coder/coder/commit/ba21ba87ba2209fad3c9f4bb131d7de1fc0e58be)
exactly as-is, but narrow the post-dial exit condition so it keys off
the controller's own context instead of the error unwrap chain.
Before:
```go
if xerrors.Is(err, context.Canceled) ||
xerrors.Is(err, context.DeadlineExceeded) {
return
}
```
After:
```go
if c.ctx.Err() != nil {
return
}
```
## Why this is the right behavior
This preserves the original graceful-shutdown invariant from
[`ba21ba87`](https://github.com/coder/coder/commit/ba21ba87ba2209fad3c9f4bb131d7de1fc0e58be)
while restoring retryability for transient transport failures:
- if `c.ctx` is canceled before dialing, the pre-dial guard still
prevents a phantom redial
- if `c.ctx` is canceled during a dial attempt, the error path still
exits cleanly because `c.ctx.Err()` is non-nil
- if a live controller hits a wrapped transport timeout, the loop no
longer dies and instead retries as intended
In other words, controller state remains the only authoritative signal
for loop shutdown.
## Non-regression coverage
This also preserves the earlier flaky-test fix from
[`ba21ba87`](https://github.com/coder/coder/commit/ba21ba87ba2209fad3c9f4bb131d7de1fc0e58be):
- `pipeDialer` still returns errors instead of asserting from background
goroutines
- `TestController_Disconnects` still waits for `uut.Closed()` before the
test exits
On top of that, this change adds focused controller tests that assert:
- a wrapped `net.OpError(context.DeadlineExceeded)` under a live
controller causes another dial attempt instead of closing the controller
- cancellation still shuts the controller down without an extra redial
## Validation
After blocking TCP connections to coderd for 20 minutes to force the
retry path, unblocking coderd allowed the daemon to recover on its own
without toggling Coder Connect.
## Summary
- add chat model pricing metadata to the agents admin form and SDK
metadata
- split pricing into its own section and show default pricing as
placeholders
- apply default pricing when admins leave pricing fields blank
This was accomplished by switching from the comboxbox (which has
deselection logic) to a select (which does not).
As a side effect, the dropdowns are wider now, which seems to match
better with other inputs anyway. And, it seems it uses the `combobox`
role instead of `button` which also seems to make more sense. Lastly,
they lose some bolding.
## Problem
When multiple tabs are open on `/agents`, every tab receives the same
WebSocket status transitions and independently plays the completion
chime — resulting in overlapping sounds.
## Fix
Use the [Web Locks
API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API)
(`navigator.locks`) to coordinate across tabs. When a tab decides a
chime should play:
1. It calls `navigator.locks.request(lockName, { ifAvailable: true },
callback)`.
2. Only the first tab to acquire the per-chatID lock plays the sound.
3. The lock is held for 2 seconds, covering any reasonable WebSocket
delivery skew between tabs.
4. Other tabs get `lock === null` and silently skip.
Falls back to immediate playback (original behavior) when the Web Locks
API is unavailable.
## Problem
1. **Personal behavior prompt not applied**: The chatd background worker
was missing `ActionReadPersonal` on `ResourceUser` in its RBAC subject.
When `resolveUserPrompt` calls `GetUserChatCustomPrompt`, the dbauthz
layer checks `ActionReadPersonal` on the user — which the chatd role
didn't have. The error was silently swallowed (returns `""`), so the
user's custom prompt was never injected into the system messages.
2. **Sequential DB calls on chat startup**: Several independent database
queries in `runChat` and `resolveChatModel` were running sequentially,
adding unnecessary latency before the LLM stream begins.
## Changes
### RBAC fix (`dbauthz.go`)
- Add `rbac.ResourceUser.Type: {policy.ActionReadPersonal}` to
`subjectChatd` site permissions
- This is the minimal permission needed — `ActionRead` on User remains
denied
### Parallelization (`chatd.go`)
Three parallelization points using `errgroup.Group`:
1. **`resolveChatModel`**: `resolveModelConfig` and
`GetEnabledChatProviders` run concurrently (both needed for
`ModelFromConfig`, which stays sequential after the wait)
2. **`runChat` startup**: `resolveChatModel` and
`GetChatMessagesForPromptByChatID` run concurrently (completely
independent)
3. **`runChat` prompt assembly**: `resolveInstructions` and
`resolveUserPrompt` run concurrently (both produce strings;
`InsertSystem` calls maintain correct order after the wait)
Same pattern applied to the `ReloadMessages` callback.
### Test (`dbauthz_test.go`)
- Add assertion in `TestAsChatd/AllowedActions` that
`ActionReadPersonal` on `ResourceUser` is permitted