mirror of
https://github.com/coder/coder.git
synced 2026-09-23 14:03:57 +08:00
632eecc09030e4eaff11481a71ea99fc71fe934f
382
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4b7494be72 |
feat: harden chat generation runtime instrumentation for billing (#27451)
Closes CODAGT-835 ## Summary `chat_messages.runtime_ms` becomes the billing source of truth for Coder Agents runtime (summed hourly by #27312), but it was built for debugging: the June refactor (#26270) silently stopped recording tool-step runtime, compaction was never measured, and interrupted turns lost their partial runtime entirely. This PR defines the billable metric, closes the paths that dropped it, and documents the definition where the data lives. ## The billable definition **`runtime_ms` is the wall-clock duration of the model invocation that produced the persisted message content**, measured from just before the provider stream opens until it is fully consumed. What counts: - Assistant generation steps, in top-level and sub-agent chats (sub-agents are ordinary chats on the same generation path). - Compaction summarization calls, persisted on the compaction assistant message (**new**). - Interrupted attempts: the message-part episode's lifetime is persisted on the partial assistant message committed by `FinishInterruption`, so partial generation time survives interruption (**new**; measured via a new `Buffer.EpisodeDuration`, which works even though the generation goroutine and the interrupt task are different tasks). What deliberately does not count (each is documented in code and docs): - **Local tool execution.** Tool wall time includes idle waits, most importantly `wait_agent` polling a sub-agent chat that already bills its own model invocations; billing the batch would double count, and excluding one tool from a concurrent batch's wall time is ill-defined. Pre-refactor instrumentation did include tool time; this makes the exclusion an explicit product definition instead of a silent regression. - **Failed model calls whose output is discarded** (retried attempts, terminal errors, content-filter refusals). They persist no content, so they bill nothing; billing errs toward undercounting. Notably a stream-silence timeout can burn 10 idle minutes before a retry, which should not be billable "active generation". If product later wants failed attempts billed, that needs a place to persist runtime on error turns (`FinishError` inserts no rows today) and is a deliberate follow-up, not instrumentation drift. - **Ancillary calls that produce no chat messages** (title generation, advisor, turn summaries) and all idle/parked time (`requires_action`, queueing). The definition is documented as `COMMENT ON COLUMN chat_messages.runtime_ms` (migration 000551, surfacing as a Go doc comment on `ChatMessage.RuntimeMs`), on `chatloop.PersistedStep.Runtime`, in the chatd architecture doc, and in the Spend Management docs page. ## Index for the hourly scan None needed: `GetTotalChatMessageRuntimeMsInRange` (#27312) filters an hour-wide `created_at` range, which the existing `idx_chat_messages_created_at` b-tree already serves; the residual `runtime_ms IS NOT NULL` filter applies to one hour of rows. A partial index would add permanent write amplification for a query that runs once an hour. > [!NOTE] > Migration 000551 is also claimed by #27312; whichever merges second renumbers via `fix_migration_numbers.sh`. ## Tests - End-to-end: the existing full-server generation test now asserts `RuntimeMs.Valid` on the committed assistant row (it previously read `.Int64` without checking `.Valid`, so it passed on NULL). - Interrupted turn: full task-level test (real DB, mock clock) asserting the partial assistant message persists the attempt's runtime. - Errored stream: asserts a failed invocation yields no step and no runtime. - Tool-using turn: asserts runtime lands on the assistant row only and tool rows stay NULL. - Compaction: asserts the summarization call duration is recorded and lands on the compaction assistant message only. - `messagepartbuffer.EpisodeDuration` unit coverage. Blocks: CODAGT-843 (B3), CODAGT-838 (D8). --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Hugo Dutka <hugo@coder.com> |
||
|
|
0ac23e3ee1 |
feat: add per-template Coder Agents access control (#27285)
Relates to CODAGT-713 Depends on #27284 This makes the per-template `agents_allowed` field authoritative in the API and chatd. It adds optional create and metadata update fields with the intended default and omission semantics, supports `agents-allowed:` template search, includes the value in telemetry, and makes `list_templates`, `read_template`, and `create_workspace` read the template row directly. Existing-workspace retries remain idempotent, and blocked same-organisation templates return an actionable message. The experimental `/template-allowlist` routes remain temporarily because the shipped AI Settings page still calls them, but they no longer control chatd enforcement. #27514 moves that page to per-template metadata, #27515 removes the legacy storage, routes, SDK types, and utility, #27517 adds the CLI flags, and #27518 updates the platform controls documentation for the per-template model, directly addressing CRF-5 and CRF-6. The stack is intended to merge as a unit. |
||
|
|
4b9880afa6 |
feat: add --chat-hook-allow-insecure to allow plain HTTP chat hook URLs (#27896)
Adds a hidden `--chat-hook-allow-insecure` / `CODER_CHAT_HOOK_ALLOW_INSECURE` deployment option (default `false`) that allows the chat lifecycle hook URL to use plain HTTP for any host. The HTTPS requirement is enforced at two points, and the flag relaxes both: `DeploymentValues.Validate()` rejects `http` hook URLs at startup, and the hook dispatcher's `validateHookURL` allows `http` only for loopback hosts. With the flag set, any-host `http` is accepted; the host, fragment/userinfo, secret, and timeout checks are unchanged, and non-http(s) schemes still fail. This removes the need for an HTTPS reverse proxy when testing a hook consumer on a trusted network. Following security review feedback, the flag description and docs state that plain HTTP lets an on-path attacker forge hook responses (which control agent execution), and `coder server` logs a startup warning (with a redacted hook URL) when hooks run over plain HTTP. Docs, generated API types, and the server config golden are updated accordingly. > Mux acted on Mike's behalf to create this PR. |
||
|
|
c62079c053 |
refactor(coderd): optimize chatdebug (#27129)
Adds a bund of optimizations to chatdebug:
In `coderd/x/chatd/chatdebug`:
- Adds a benchmark (excluding LLM and database stuff)
- Replaces string concatenation with strings.Builder when accumulating
stream parts (~105,000ns -> ~50,00ns)
- Removes double JSON encode in RecordingTransport (114,000ns ->
64,000ns)
In `coderd/util/strings`:
- Adds a benchmark for Truncate
- Removes unnecessary allocations in Truncate (~110,000ns -> 1,550ns in
truncation case, 1 alloc -> 0 allocs in no truncation case)
> 🤖 Claude helped with this.
|
||
|
|
1702bbb816 |
test(coderd/x/chatd): accept query cancellation in subagent wait (#27818)
Closes CODAGT-877 Closes https://github.com/coder/internal/issues/1437 The linked flake happens because the context deadline can be observed as `context.DeadlineExceeded`, or as a PostgreSQL query cancellation when a database call is in flight. The fix is just to assert that the deadline expired and the returned error is a recognised query cancellation, rather than depending on which layer notices it first. |
||
|
|
6b8f820493 |
feat: remove native chat cost tracking in favor of AI Gateway cost data (#27330)
## Stack Context This stack makes AI Gateway data and budgets the source of truth for AI spend controls. 1. Re-back the per-chat cost endpoint with AI Gateway data (#27328, merged). 2. Remove native chat usage limits (#27329, merged). 3. **This PR, now based on `main`:** remove native chat cost tracking and its dedicated admin UI. ## Summary Removes native per-message price calculation, model pricing fields, cost persistence, aggregate cost queries, and admin cost API types. It also deletes the Analytics and Spend pages plus their legacy redirects. The AI Gateway-backed per-chat cost row and compact budget indicators remain. The spend documentation is renamed to `spend-management.md` and updated for the remaining surfaces, group budget APIs, CSV export, upgrade handling for native pricing and cost history, and the absence of a deployment-wide spend dashboard. The per-chat cost API documents that data follows AI Gateway retention and reports zero after all matching requests are purged. No schema is dropped in this release. `chat_messages.total_cost_micros` remains nullable and unwritten so replicas from the previous release can continue inserting messages during rolling upgrades. #27600 tracks removal after the compatibility window. > Mux prepared this PR on Mike's behalf. |
||
|
|
f0e6ac64b3 |
feat: remove native chat usage limits in favor of AI Gateway budgets (#27329)
## Stack Context This stack makes AI Gateway data and budgets the source of truth for AI spend controls. 1. Re-back the per-chat cost endpoint with AI Gateway data (#27328, merged). 2. **This PR:** remove native chat usage limits. 3. Remove native chat cost tracking and its dedicated admin UI (#27330). ## Summary Removes the native usage-limit API, SDK types, SQL, and chat enforcement for deployment, user, and group chat limits. Compact AI Gateway budget indicators remain in the Agents sidebar, user menu, and group settings. Gateway budget rejections and provider quota failures continue to classify as usage-limit errors, including a 409 response for synchronous title generation. Budget-period labels now use the API's UTC boundaries, so users see the same dates in every browser timezone. The documentation explains the AI Gateway replacement, its licensing requirements, and the differences from native limits. No schema is dropped in this release. The usage-limit table, index, user and group columns, constraints, audit mappings, and generated scan fields remain for mixed-version rolling upgrades. #27600 tracks their removal after the compatibility window. ## Breaking change Native day, week, and month chat spend limits are removed and are not migrated. AI Gateway budgets are month-based, group-scoped with per-user overrides, and require the AI Gateway entitlement. Deployments without that entitlement no longer have chat spend enforcement. > Mux prepared this PR on Mike's behalf. |
||
|
|
404bb2f663 |
refactor(coderd/x/chatd): own provider option construction in one function (#27705)
Stacked on #27704. Provider option conversion and reasoning effort injection both create OpenAI option structs, so each of the four call sites had to pair them in the right order and pick the same transport for each. `chatprovider.ProviderOptionsForCall` now owns both steps, and the two helpers it wraps are unexported. The advisor, main generation, compaction override, and quickgen paths each collapse to one call. The ARCHITECTURE section on transport selection is updated to match: `ProviderOptionsForCall` is described as the only entry point in `chatprovider` that builds provider options for a call, delegating OpenAI conversion to `chatopenai.ProviderOptionsFromChatConfig`, and it now records that the quickgen turn status label and chat summary paths deliberately send no provider options. > Mux prepared this PR on Mike's behalf. |
||
|
|
0e16e356b0 |
refactor(coderd/x/chatd): read the OpenAI transport from the model (#27704)
Stacked on #27703. Provider option conversion, reasoning effort injection, and file part acceptance each recomputed the OpenAI wire format from `(provider, modelID, override)`. They now read it from `chatprovider.Model`, so a decision cannot drift from the client it was built for. `ProviderOptionsFromChatConfig` takes a `Transport`, `ApplyReasoningEffort` takes a `Model`, and `AcceptsFilePartMediaType` becomes a `Model` method. `UsesResponsesAPI` and `UsesResponsesOptions` are deleted. The override extraction is unexported and reachable only from `ModelFromConfig`, which now takes the model's `ChatModelOpenAIConfig` directly, removing the six scattered extractions at call sites. That also resolves the computer-use mismatch. The computer-use model is a hardcoded default with no config row of its own: its client was built without an override while request preparation applied the chat model's. Preparation now reads the computer-use model's own transport, so the two agree without one model's client settings following a different model. Passing the chat model's `openai_config` into the computer-use client would have made them agree on the wrong value. `TestModelTransportConsumersAgree` pins the invariant in one test: the HTTP path the client actually hits, the concrete provider option struct type, the type created by reasoning effort, and text/image file acceptance. > Mux prepared this PR on Mike's behalf. |
||
|
|
b5c9e8e471 |
refactor(coderd/x/chatd): carry the resolved OpenAI transport on a model wrapper (#27703)
Stacked on #27683. The OpenAI wire format is decided when the client is built, then thrown away, so downstream sites recompute it from `(provider, modelID, override)`. Any disagreement fails silently: the SDK type-asserts the concrete provider options struct and discards every OpenAI option, and text attachments are dropped because Responses natively accepts only images and PDFs. This adds `chatprovider.Model`, which pairs a fantasy client with the transport resolved from that client's own identity. Its fields are unexported and only the constructor sets the transport, deriving it from the client, so no caller can pick a transport that disagrees with the client it wraps. `chatopenai.Transport`'s zero value is invalid and panics when read rather than defaulting to a wire format, following the existing precedent for construction invariants. `Model` is threaded through construction, the resolve paths, and the four struct fields that store a model for later request preparation. Terminal call sites keep taking `fantasy.LanguageModel` and receive `LanguageModel()`, which avoids a new package edge from `chatloop` and `chatadvisor` into `chatprovider`. No decisions move yet. The consumers still recompute the transport, and `UsesResponsesAPI` now delegates to `TransportFor` so the two agree by construction. #27704 makes the consumers read it from the model. > Mux prepared this PR on Mike's behalf. |
||
|
|
c6cee10e8b |
feat: add per-model OpenAI Responses API toggle (#27683)
chatd hardcoded `WithUseResponsesAPI()`, so the provider SDK's static known-model list decided whether an OpenAI model spoke the Responses API or Chat Completions. A model absent from that list silently fell back to Chat Completions until the fantasy fork was patched. This exposes the SDK's `WithResponsesAPIFunc` hook as a per-model setting, `openai_config.use_responses_api`, stored in the existing `chat_model_configs.options` JSONB. Unset keeps the known-model list, `true` forces Responses, `false` forces Chat Completions. There is no migration. It sits in a new construction-time `openai_config` section rather than in `provider_options.openai` because it selects the API when the client is built, while `provider_options` holds per-request parameters. That placement is also load-bearing: a config setting only this field would otherwise materialize an OpenAI request-options struct and turn on provider-side response storage, since `Store` defaults to true there. Three places independently decided the transport and would silently disagree with the client actually built: | Site | Effect when it disagrees | | --- | --- | | `ModelFromConfig` | the transport being overridden | | `AcceptsFilePartMediaType` | text attachments dropped, since Responses natively accepts only images and PDFs | | `UsesResponsesOptions` | the SDK type-asserts the concrete options struct, so every OpenAI option is discarded | They share one predicate here, `chatopenai.UsesResponsesAPI`, with the override threaded to each. The rest of the stack removes that threading by resolving the transport once and carrying it. Compaction overrides and the quickgen debug model built clients without `ConfigOptions`, so they now pass it and pick up both this setting and the existing Anthropic beta headers. The toggle also makes transport-conditional option handling admin-switchable, so two hardening changes ride along. `ServiceTierFromChat` now maps every tier the codersdk enum advertises (`auto`, `default`, `flex`, `scale`, `priority`); it previously returned nil for `default` and `scale`, so flipping a model to Responses silently dropped a configured `service_tier` that the API accepts (fantasy forwards the value unchanged). And a new `TestProviderOptionsTransportParity` pins, per `provider_options.openai` field, which transport honors it, against a table in ARCHITECTURE.md, so a field honored on one transport and silently ignored on the other fails the test unless recorded as intentional. Review rounds also caught two lifecycle gaps around the new field. `isZeroChatModelCallConfig` now inspects `OpenAIConfig`, so a stored options blob whose only setting is this toggle survives into GET/list responses instead of reading as `model_config: null`; `TestIsZeroChatModelCallConfigCoversEveryField` sets each config field in isolation and fails if any field is invisible to the zero check. And the model editor's update path sends an explicit empty `model_config` when an edit clears the last field, since an omitted property preserves the stored options server-side; covered by the `EditClearingLastOptionSendsEmptyConfig` story. Azure keeps following the known-model list, because the Azure provider exposes no equivalent hook. The model editor renders Azure with the OpenAI option schema, so instead of shipping a visible but inert control, the option schema generator gains a `providers` struct tag that it emits as `visible_for_providers`. Gating uses the raw provider type rather than the alias table, so the control appears only for openai-typed providers. No hand-written frontend field: the editor renders it from the generated schema. Closes https://linear.app/codercom/issue/CODAGT-874/add-completionsresponses-api-toggle-in-model-editor > Mux prepared this PR on Mike's behalf. |
||
|
|
fc24c27dfd |
fix: reserve chat hook dispatch capacity for running turns (#27656)
## Context Follow-up fix from live UAT of the merged chat lifecycle hooks stack (#27430). Its companion UAT fix (#27655) has merged, so this targets `main` directly. ## Why? UAT measured a burst of 1,500 concurrent chat creations against a consumer with 1.2s latency. 255 were admitted and 1,245 got `502 hook_dispatch_failed (over_capacity)`, which is correct fail-closed behavior. The collateral wasn't: the same burst failed 24 `stop` dispatches, parking chats that had already been admitted and had already executed tools. One 256-slot semaphore served every event, so new-work admission could take every slot and kill turns in flight. Callers now classify each dispatch as admission or generation, and admission draws from a 192-slot gate held *before* the shared pool. At least 64 shared slots stay reachable only by dispatches for work a chat already admitted. The dispatcher is per `coderd` replica, so these limits are per replica, not deployment-wide, and the docs say so. **The caller classifies, not the event type.** Event type isn't a reliable proxy in either direction: a subagent spawn dispatches `user_prompt_submit` from inside a running turn, and the edit path dispatches `session_start` at admission time. `CapacityClassUnset` is rejected in `Dispatch`, so a new call site fails closed rather than silently inheriting a share. **Acquisition order is load-bearing.** Admission takes its own gate first. Taking a shared slot first would let admissions queued on the gate occupy the very capacity the reserve protects. `acquireCapacity` is the only path that takes either pool, so the order can't be bypassed. ## What this does not guarantee Nothing bounds how many turns generate concurrently, so the 192/64 split is a judgement call, not a derived ceiling. This stops an *admission* burst from consuming every slot; it does not make the remainder sufficient. A large enough generation load can still exhaust the reserve and error a running chat. The docs say so explicitly rather than promising a guarantee the code doesn't deliver. Generation can now take all 256 slots, so generation traffic starves admission harder than before. That's the intended priority: rejecting a new prompt is recoverable, ending a turn that already ran tools is not. ## Testing Red-green proved both new tests. Removing the release-on-failure path fails `RefusedSharedAcquireReleasesAdmission` deterministically; removing the expired-deadline check fails `ExpiredDeadlineRefusesFreeSlot` in 18/30 runs. That deadline check fixes a real race found in review. `acquire` previously shared one `time.Timer` across both acquires. Because `select` picks a ready case at random, an admission dispatch could take a slot after its capacity deadline had passed. Measured over 300 trials: 135 late acquisitions, worst overshoot 2.1ms. `acquire` now takes an absolute deadline and refuses an expired one before selecting, which measures 0/300. Go: `coderd/x/agenthooks/...` and `coderd/x/chatd/...`, plus `-race -count=3` on the dispatcher. > Mux opened this PR on Mike's behalf. |
||
|
|
df1c0f9710 |
feat: show what a chat lifecycle hook changed (#27655)
## Stack Context Follow-up fixes from live UAT of the merged chat lifecycle hooks stack (#27430). Two PRs: 1. **This PR**: make hook effects visible and correctly attributed in the transcript. 2. [`mike/chat-hooks-uat/dispatch-capacity`]: reserve dispatch capacity so an admission burst can't fail running turns. ## Why? UAT found three ways the transcript misrepresented what a lifecycle hook did. All three are user-visible and share the same surface (`chathooks/effects.go`, `codersdk.ChatMessagePart`, the conversation timeline), so they're reviewed together. **A prompt `input_override` silently discarded attachments.** `ComposeUserPromptContent` replaced the entire submitted part list with one text part, dropping `file` and `file-reference` parts along with their `chat_file_links`. The user saw their attachments vanish with no explanation. The override now replaces submitted *text* parts only and preserves non-text parts in order. A consumer that wants to block attachments uses `deny`, which is the documented mechanism for refusing a submission. **Every user-visible `system` row was labelled "Lifecycle hook".** The timeline keyed the notice off `role === "system"`. That was correct only by accident, because the hook `user_message` was the sole client-visible system row. The backend now emits the notice as a typed `hook-notice` part and the timeline renders on that, so a future system row can't be mislabelled as a policy notice. **Nothing marked a tool call the hook had rewritten.** A consumer could replace tool input via `input_override` and the transcript showed the rewritten input as if the model had produced it. `ChatMessagePart` gains `hook_rewritten`, set from `preflight.Overrides` on the same path that already carries `ToolCallCreatedAt`, and the tool row renders a "Modified by policy" badge. `ToolCall.PolicyProvider` renders the badge itself, at four wrap sites: the `Tool` dispatch wrapper, the `ReadFilesTool` aggregate and its per-file rows, and `ReadFileTimelineBlock` (grouped and single `read_file` rows bypass `Tool`). Renderer props do not include the flag; descendants consume it through the provider context. The badge is emitted by the provider rather than by the shared header because several renderer branches return early without one, including the auth-required `execute` card, a completed `ask_user_question`, and an empty question payload. Those branches would drop the attribution with no type or runtime error, and the gap is not greppable: every renderer file contains a header somewhere, only individual branches do not. Emitting at the provider removes the possibility instead of enumerating the cases. A rewritten call is wrapped in a group labelled by its badge, so one rewritten file inside a merged read is attributed on its own rather than inheriting the group's badge. `HeaderButton` still appends the policy wording to an explicit `ariaLabel`, since an explicit `aria-label` replaces the name computed from descendants. Provider-executed calls are excluded from attribution. Hooks never see them, and duplicate tool-call ID rejection deliberately skips them, so a reused ID would otherwise mark a provider-executed call as policy-rewritten. ## Testing Go: `coderd/x/chatd/...`, `coderd/x/agenthooks/...`, `codersdk/...`, and `coderd -run 'Hook|Chat'`. Frontend: `tsc` plus every `AgentsPage` story; the only failures are `MCP Tool Completed` and `Scroll To Bottom Button Works With Inverse Scroll`, both of which fail on trunk. A registry-wide story asserts every registered renderer shows the badge, verified against three inverted toggles: removing the badge, hiding it with `display:none`, and skipping the provider for one renderer (which names that renderer). Storybook also covers the rewritten subagent spawn, a completed empty question payload, a non-hook system message, and a failed `read_file` guarding the accessible name. > Mux opened this PR on Mike's behalf. |
||
|
|
c17bed25e0 |
feat: wire chat lifecycle hooks into chatd (#27429)
Wires chat lifecycle hooks into chatd, gated by the `agent-lifecycle-hooks` experiment. Part of the lifecycle hooks stack (#27401, #27428, #27430). See `docs/admin/setup/chat-lifecycle-hooks.md` for the consumer-facing contract. ## Summary When a hook URL is configured, chatd dispatches `session_start`, `user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `pre_compact`, `post_compact`, and `stop` events to the consumer and applies its responses. ## Design - **Stateless**: Coder stores no hook dispatch or decision state. Delivery is at least once; consumers deduplicate on stable payload identifiers (chat ID, event type, tool-use ID) and answer duplicates with the same decision. - **Admission-time prompt effects**: `user_prompt_submit` dispatches exactly once per submission (create, send, queue, edit, subagent spawn) and folds its effects into the stored prompt as typed message parts: original-or-overridden user parts, then model-only `hook-context`, then a user-visible `hook-notice`. Hook context is stripped from every client-facing conversion; hook notices are excluded from model prompts. The server rejects hook parts in client-submitted content. - **Tool gating**: `pre_tool_use` allow can override tool input; deny becomes a synthetic denied tool result, with any returned model context persisted as a model-only transcript row so it never reaches clients. The denial text identifies an external policy (the deployment's lifecycle hook) as the source and marks the decision as persistent, so the model explains the denial instead of retrying it or misreporting it as an infrastructure failure. - **Fail closed**: a dispatch failure rejects the triggering request or moves the chat to the error state in the same transaction as the affected step, so a runnable state is never published with unapproved content. - **Admission before persistence**: `pre_tool_use` is dispatched for the calls the model produced, before the assistant message is stored. See "Staged tool admission" below. - **Fresh dispatch per tool call**: every non-provider-executed tool call is decided by its own `pre_tool_use` dispatch; Coder never reuses an earlier decision on the consumer's behalf. Retries re-dispatch the same logical event. ## Structure All hook dispatch flows through one seam: entry points build a `chathooks.Chat` (chat identity) and a `chathooks.Message` (event details) and call `Trigger.Trigger`, the only component that talks to the dispatcher. The integration lives in the `coderd/x/chatd/chathooks` subpackage, split by responsibility: - `trigger.go`: the trigger seam; builds the wire envelope per event, normalizes deny into a typed error, and holds the package's single enabled-check. - `effects.go`: pure conversion of hook results into transcript rows and prompt parts. - `errors.go`: failure classification (dispatch error messages, denial mapping, tool-result dispatch-failure scanning). - `tooluse.go`: the tool-call gate (`pre_tool_use` preflight, `post_tool_use` payloads, applying admitted input to the step). Server-bound glue stays in `coderd/x/chatd/hook_server.go`: the chat-parking dispatch error handlers, the step-commit row insertion wrappers, and the dynamic post-tool-use state loader, which depends on chatd validation types. This PR adopts the `codersdk/x/agenthooks` and `coderd/x/agenthooks/dispatch` import paths introduced at the tip of #27401; intermediate commits still reference the pre-move paths and are not individually buildable. ## Staged tool admission `pre_tool_use` originally ran at tool execution time, which is after the assistant message carrying the tool call was already committed. An `input_override` therefore had to rewrite stored message content in place. @hugodutka pointed out that chatd treats message content as immutable, and that the rewrite was a shortcut rather than a requirement. It was also a correctness problem in its own right: the rewrite only updated the database, so the transcript could show one input while a different one had executed. The hook now runs before the step is persisted: ```text provider stream ends (tool calls complete, in memory) -> pre_tool_use dispatch per call -> ONE transaction: assistant row with admitted inputs, synthetic denials, hook rows -> execute ``` The step is inserted once, carrying the input the tool runs with. `UpdateChatMessageContentByID` and `Tx.UpdateMessageContent` are deleted from #27428, so message content stays immutable. Two consequences, both intentional: - **Clients converge rather than wait.** Tool-call parts still stream live, so a rewritten call briefly shows the model's proposed input before the committed message replaces it. The chat store already clears stream state when an assistant message arrives, so the stored input wins with no frontend change and no added latency before tool cards appear. - **A call already in history was already admitted.** Execution consumes the stored input instead of dispatching a second decision, which keeps one dispatch and one set of hook effects per call. A consumer policy change between admission and execution applies to later calls, not to calls already admitted. The per-chat debug endpoint still records the provider's original tool input. Its purpose is to report provider behavior, and it requires an explicit per-chat debug flag; the invariant here covers the transcript. ## Configuration Adds `chat-hook-url`, `chat-hook-secret`, `chat-hook-timeout`, and `chat-hook-enabled` deployment options with startup validation. The flags are hidden from `coder server --help` while the feature is experimental; the setup guide documents them. ## Tool input validation Built-in tool arguments reach a consumer as raw JSON with key spelling preserved, but the tools decode those bytes with Go, which matches struct fields case-insensitively and keeps the last match. A policy reading `path` could therefore authorize one value while the tool executed another, and a lone case variant such as `{"PATH":"/secret"}` was invisible to a policy checking for `path`. Coder now rejects a built-in tool call whose input repeats a key or spells a schema property with different capitalization, before the `pre_tool_use` dispatch, so a consumer is never asked to authorize bytes whose meaning depends on the reader. Rejected calls produce an error result the model can retry; unambiguous calls in the same batch still run. A consumer-authored `input_override` is rechecked after the dispatch and fails the turn closed, because the model cannot correct it. Dynamic and MCP inputs are excluded because the client and the workspace agent execute those calls rather than coderd. Two paths needed more than a schema check. Execution resolves a deprecated tool name to its canonical tool, so validation resolves aliases first. The `edit_files` decoder also reads `search` and `replace`, which its schema does not advertise, so those aliases are now matched exactly and their case variants ignored. A hook denial now returns a structured 403 carrying `kind: "hook_denied"`, mirroring the dispatch-failure response that already carries its own kind. Without it a client cannot tell a policy decision apart from a generic failure, and the chat UI titled a denial "Request failed". Adding a kind needs no migration: `ChatErrorKind` is persisted only inside the JSONB `chats.last_error` column, whose decoder accepts unknown kinds. The hook docs also correct the tool-input convergence window. A batch dispatches sequentially before the assistant row commits, so the original input stays visible for a span that scales with the number of tool calls in the step rather than a single hook timeout. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
91c7232d97 |
feat: add chat suffix messages, idle failure, and content update support (#27428)
Adds generic chat state and query capabilities that the lifecycle hooks integration (#27429) builds on. Part of the lifecycle hooks stack (#27401, #27429, #27430). - `chatstate`: `EditMessage` accepts caller-provided suffix messages inserted after the replacement in the same transaction, transitions can carry a typed error kind, and `FinishError` is also allowed from waiting chats so admission-time failures can park an idle chat in error. - `chatstate`: `ValidateToolResults` holds the submitted-tool-result rules (duplicate, invalid JSON, missing, unexpected) in one place, so `CompleteRequiresAction` and API-level prechecks reject the same payloads with the same typed causes. - `database`: `InsertChat` accepts an optional caller-provided ID. No hook-specific state or behavior is introduced here; these primitives are usable by any caller. An earlier revision added a message-content rewrite primitive so a `pre_tool_use` override could update an already-committed tool call. Message content is immutable by design, and @hugodutka pushed back on changing that. The rewrite is gone: #27429 now dispatches the hook before the assistant message is stored, so the stored input is the one that runs and nothing needs updating. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
5d2a69d85a |
fix(coderd/x/chatd/chaterror): extract plain-text provider error bodies (#27597)
Follows up #27538. Fixes an issue where `chaterror` would not classify a plain-text aibridge budget error 403 as ChatErrorKindUsageLimit. Root cause: Anthropic adapter drops the body from `ProviderError.Message`, and `providerErrorResponseMessage` extracted only JSON. - Parses the dumped response with `http.ReadResponse` (strips headers, de-chunks, leaves non-dump payloads like Google's raw messages whole). - When JSON extraction yields nothing, falls back to the trimmed first line of the body. - Falls back on `Content-Type: text/plain` only, and never on valid JSON as `Detail` is user-facing. - Adds an end-to-end regression test that Anthropic-shaped 403 budget error → `usage_limit`, not retryable (was `auth`). - Adds table-driven test cases: HTML skipped, whitespace skipped, first-line-only, JSON-without-message, chunked, Google raw message. --- > Generated by Coder Agents on behalf of @johnstcn. |
||
|
|
e96d8646e2 |
fix(coderd): give chat message ids an append-order guarantee (#27495)
Chat message ordering was derived from `created_at`, which is `now()` and therefore the transaction start time. That makes it unusable as an append-order column for two independent reasons: every row in one `InsertChatMessages` batch shares a single timestamp, and two concurrent transactions can commit in the opposite order to the one they started in. This PR gives `chat_messages.id` a real append-order guarantee and moves the history reads onto it. ## Changes **`InsertChatMessages` had no input-order guarantee.** Callers index the returned slice by input position. That only worked because PostgreSQL happens to evaluate the `BIGSERIAL` default in row order. Ids are now allocated up front and the k-th smallest is assigned to input index k, so the pairing does not depend on where the column default is evaluated. Returned rows are explicitly `ORDER BY id`. **Three history reads now order by `id`.** | Query | Was | Now | |---|---|---| | `GetChatMessagesByChatID` | `created_at ASC` | `id ASC` | | `GetChatMessagesByRevisionForStream` | `created_at ASC, id ASC` | `id ASC` | | `GetLastChatMessageByRole` | `created_at DESC, id DESC` | `id DESC` | `GetChatMessagesByChatID` paginated by `id` while ordering by `created_at`, which is incoherent on its own terms. The other two matter because of who consumes them. The stream query supplies incremental updates on the same socket that emits a full `GetChatMessagesByChatID` snapshot on history reset, so once that snapshot moved to `id` the two disagreed under timestamp skew. `GetLastChatMessageByRole` returns an id that is then used as an id cursor, both as `AfterID` when synthesizing tool cancellations and as `chats.last_read_message_id`, where a stale anchor leaves later assistant messages permanently unread. A tie-breaker would not have fixed either one. It only resolves equal timestamps; leading with `created_at` is the actual defect. **`GetLastChatMessageByRole` loses its index, so this adds one.** `ORDER BY created_at DESC, id DESC` could take an ordered scan of `idx_chat_messages_chat_created`. Nothing in the schema can supply `ORDER BY id DESC LIMIT 1` for a given `chat_id` and `role`, so the planner switches to a backward scan of the primary key and filters every newer row in the table, scanning all of it when the chat has no message in that role, which is the routine case for a fresh chat. Migration `000559` adds `(chat_id, role, id DESC) WHERE deleted = false`, the same shape as the existing `idx_chat_messages_user_prompts`. This matters because the query is hot: it runs on every stream connect and disconnect, and once per turn when synthesizing tool cancellations. `GetChatMessagesForPromptByChatID` has the same defect and is fixed in the stacked PR, because its compaction boundary change is semantic and deserves a separate review. Auto-archive stays timestamp-based deliberately: it measures activity, not order. Wrapping the insert in a CTE (needed because `INSERT` cannot take `ORDER BY`) makes sqlc synthesize `InsertChatMessagesRow`. It is structurally identical to `ChatMessage`, so the call sites use a direct struct conversion that stops compiling if the two ever diverge. ## Testing Behavior tests write `created_at` values inverted against id order, so a reader that leads with `created_at` returns the batch backwards. All three queries were verified red by reverting the `ORDER BY` and regenerating: the stream query returned `[3,2,1]` for `[1,2,3]`, and `GetLastChatMessageByRole` picked id 1 instead of id 3. `TestInsertChatMessagesOrderContract` asserts against the generated SQL, covering what a behavior test cannot: PostgreSQL evaluates the id default in row order anyway, so a batch still looks ordered once the guarantee is removed. `TestChatMessagesSequenceCacheIsOne` guards the cross-batch half of the invariant. Ids follow chat row lock order only while the sequence hands out one value at a time; sequence cache blocks are per session, so with a cache above one a backend holding stale cached values can lock second and still commit lower ids. Bumping a sequence cache is an ordinary throughput tweak, and it would silently corrupt history order. The index was checked on a 200k row fixture. Without it, the zero-match lookup filters all 200,000 rows over 2763 buffers; with it, the plan is an index scan with both `chat_id` and `role` in the index condition, no sort node, and 3 buffers. Note that the within-batch mapping does not depend on the cache size. It is established by `ROW_NUMBER() OVER (ORDER BY id)` over the allocated ids, so it holds regardless of `nextval` evaluation order. ## Note on the deleted subagent hand-sort The subagent history reader's hand-sort stays deleted, but calling it redundant was imprecise. It sorted by `created_at` then `id`, so it is only equivalent to `id` ordering when the two agree. When they disagree the old code selected a different "latest assistant". This is a deliberate behavior change to match the new invariant, not dead-code removal. > Opened by Mux on behalf of Mike. |
||
|
|
75fd7bc09a |
fix: remove chatd usage limit enforcement (#27535)
This PR surgically removes enforcement of Agents spend limits: - Adjusts the relevant function that checks usage to always return nil - Deletes tests that expect a usage limit error. |
||
|
|
8ea2586189 |
feat: add chat lifecycle hook dispatch backend (#27401)
Adds the chat lifecycle hook wire contract and dispatch plumbing, first PR of the lifecycle hooks stack (followed by #27428, #27429, #27430). - `codersdk/x/agenthooks`: event and response wire types, JWT creation and verification with the shared secret (HS256, request body digest, expiry and not-before freshness checks), and an HTTP handler helper so consumers only implement the events they use. The `codersdk/x` location marks the consumer SDK as experimental. - `coderd/x/agenthooks/dispatch`: a stateless dispatcher that signs and posts hook events, enforces a concurrency cap under one configured timeout that bounds both the capacity wait and both post attempts, retries one connection failure with the same JWT, sends a distinctive `coderd-agenthooks/<version>` User-Agent, and records Prometheus metrics. Delivery is at least once; consumers own durable decision state, audit records, and deduplication keyed by the stable payload identifiers. Nothing is persisted by Coder. - Response bodies decode strictly: unknown fields, duplicate JSON keys (including inside `input_override`), and trailing data fail the dispatch closed as protocol errors instead of silently reading as allow. - `coderd/util/xnet`: shared timeout and connection error classification used by the dispatcher retry logic. Transient HTTP/2 stream aborts count as connection errors, so the documented single retry also applies to h2 consumers, which is the shape Go's default transport negotiates against any TLS consumer. Deterministic protocol failures stay terminal. Only the struct form of a stream error is matched, because `net/http` bundles its own HTTP/2 types and `h2_error.go` bridges only that shape. - `scripts/agenthooks-server`: a reference consumer that logs events and demonstrates consumer-owned pre-tool decision deduplication. It requires an explicitly configured JWT audience rather than deriving one from the request, and its startup output names the mode it is running in so an operator can see that the example policy flags need `-log-only=false`. - `scripts/apitypings`: generate TypeScript types for the hook wire contract. Dispatch failures log without the error's stack frames, since a failed dispatch is an expected, operator-visible condition. Nothing dispatches these events yet; chatd wiring lands in #27429. > This PR was written by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
daf655dff8 |
fix(coderd/x/chatd/chaterror): classify aibridge 403 as ChatErrorKindUsageLimit (#27538)
Adds the string `ai budget` to the classifier for `ChatErrorKindUsageLimit`. <img width="809" height="267" alt="Screenshot 2026-07-27 at 18 27 13" src="https://github.com/user-attachments/assets/7e2ba9ae-8168-4fd4-87f6-c4e7dfdc9526" /> Testing notes: - I set the group limit by running `insert into group_ai_budgets values ('<everyone group UUID>', 1, NOW(), NOW());` > Created by a human, trimmed down by a Coder agent. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
b67e1b24f9 |
fix(coderd/x/chatd): avoid request recorder race (#27525)
`TestActiveServer_BasicAssistantGenerationAndPromptPreparation` could race by reassigning a request recorder captured by concurrent callbacks. Keep the recorder immutable across both scenarios. Closes https://github.com/coder/internal/issues/1626 |
||
|
|
6f2011af88 |
feat: add chat summary tab in the right sidebar and per-chat cost endpoint (#26649)
Stacked on #26657 (the persisted whole-chat summary backend). Base branch is `chat-summary-62j9`; review/merge that first. Adds a reusable `ChatSummary` component. The summary text is the persisted whole-chat summary (`chat.summary`) introduced by #26657. It is generated asynchronously and may be `null` until the first summary is produced, in which case the popover renders a muted empty state. Live updates arrive via that PR's `chat_summary_change` watch event, which is already merged into the chat caches. Cost is served by a new per-chat endpoint, `GET /api/experimental/chats/{chat}/cost`, which rolls up assistant-message cost across a chat's root and child (subagent) chats and is authorized like the other `{chat}` routes (read on the chat, 404 otherwise). Visual and interaction coverage lives in `ChatSummary.stories.tsx` and `ChatSummaryPopover.stories.tsx` (including populated-summary, empty-state, and cost-loading cases). --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6159eb4fc5 | feat(coderd/x/chatd): add structured error fields to wait_agent error payload (#27478) | ||
|
|
ff10beb042 | fix(coderd/x/chatd): surface child error detail in wait_agent last_error (#27477) | ||
|
|
e9951b07d4 |
fix(coderd/x/chatd/chatloop): surface reasoning-only content-filter refusals as terminal errors (#27476)
## Problem Anthropic can end a stream with `stop_reason: "refusal"` after reasoning content has already streamed. The content-filter guard in `chatloop.GenerateAssistant` only fired when the step content was completely empty, so a reasoning-only refusal bypassed it: the turn finished as `status=waiting` with `last_error=null`, and the user saw the chat silently stop mid-turn with no explanation. This looked like a Coder fault when the provider had rejected the response. Observed twice in dogfood on 2026-07-23 (chat `c72f99fc`, debug steps show `finish_reason=content-filter` with reasoning-only content). ## Change Treat a content-filter finish as terminal whenever the step produced no user-visible output. A new `hasUserVisibleContent` helper counts any non-reasoning part (text, tool call, tool result) as user-visible; reasoning-only or empty steps now return the existing `contentFilterError`, which flows through the established pipeline: classified `ChatErrorKindContentFilter` (non-retryable, refusal category/detail when provided), persisted `chats.last_error`, streamed error event, and the "Response blocked" callout in the chat UI. Behavior for steps with visible text or tool calls is unchanged, and the frontend needs no changes. ## Testing - New regression subtest `ReasoningOnlyContentSurfacesTerminalError` (reasoning stream then content-filter finish) beside the existing empty-content and partial-content subtests, which are unchanged. - `go test ./coderd/x/chatd/...` and lint pass. - Dogfood UAT against a local dev instance with a mock Anthropic upstream passed all three scenarios: reasoning-only refusal shows the "Response blocked" callout with `last_error.kind=content_filter` and no retry affordance; text-then-refusal still completes normally; empty refusal still errors. > This PR was created by Mux acting on Mike's behalf. |
||
|
|
3c7a1d33e3 |
feat: add persisted whole-chat summary with background generation (#26657)
Adds a persisted whole-chat summary that backs the chat summary popover. A new nullable `chats.summary` column is populated in the background after a successful root-chat turn and pushed to clients via a new `chat_summary_change` watch event (distinct from `summary_change`, which is bound to `last_turn_summary`), so the popover reads `chat.summary` straight off the loaded `Chat` with no extra query. This is the data source for the popover and per-chat cost UI built in #26649; the popover can consume `chat.summary` once this lands (the field is nullable, so merge order does not matter). ## How it works - **Generation** runs in the existing successful-turn finalize hook, detached from the request so the user's turn is never blocked. A cadence gate generates the first summary after one completed turn, then regenerates every three turns, using the `chats.summary_generated_at` freshness marker. Generation reads compaction-aware history, renders it to a bounded plain-text transcript (short transcripts are skipped), and asks for a 1-3 sentence summary via structured output. Failures never clear an existing summary. - **Staleness** is guarded by `history_version` (mirroring `last_turn_summary`), so a background write racing a newer turn loses while worker lifecycle transitions cannot reject a fresh write. - **Model selection** uses the chat's configured model. ## Deferred to follow-ups - **Cost accounting**: the `chat_messages.cost_source` discriminator and summary/title usage recording were removed from this PR so summary persistence is not blocked by hidden accounting rows advancing `history_version`. Title usage recording stays on main's `InsertChatMessages` path. - **Model override**: deployment-wide summary generation model selection is split into #26803; the base feature always uses the chat model. ## Notes - Migration `000540` adds `chats.summary` and `chats.summary_generated_at`, and recreates `chats_expanded` to expose the new columns. - Root chats only; shared viewers pick up the summary on their next refetch (live watch events are owner-only). Refs #26649 --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
02fd1cc691 | feat: allow spawn_agent model and reasoning effort override (#27385) | ||
|
|
aa89801ee5 |
fix(coderd/x/chatd/chatadvisor): textualize advisor prompt tool exchanges (#27059)
Closes CODAGT-592.
## Problem
The advisor tool sometimes fails with the opaque error `advisor produced
no text output`. Live reproduction against `claude-sonnet-4-6` showed
the cause: `BuildAdvisorMessages` forwards the parent conversation's raw
`tool_use`/`tool_result` blocks into the nested advisor call, which
defines no tools. The nested model imitates the forwarded pattern and
spends its turn committing to a tool call it cannot make (captured
reasoning from a failing run: "The user wants me to make another tool
call to the advisor about writing a poem about cucumbers."), so the step
ends with reasoning-only or empty content and no advice. Because each
chat step currently rebuilds the advisor runtime and snapshot
(CODAGT-593), the second advisor call in a run reliably sees the first
call's exchange, which is why the first call succeeds and later ones
fail.
## Fix
- `BuildAdvisorMessages` rewrites tool activity as plain-text notes:
assistant tool-call parts are removed and folded, together with their
matching result, into a single user-role note of the form `[The parent
agent ran the X tool with input {...}. Result: ...]`. No raw tool blocks
and no bare call lines reach the tool-less nested request. This also
removes the provider requirement that `tool_result` blocks pair with a
`tool_use`, so results orphaned by window truncation are kept as notes
instead of dropped.
- The `advisor produced no text output` error now appends the finish
reason and content-part kinds, e.g. `advisor produced no text output
(finish_reason=stop; parts: reasoning=1)`, so field reports distinguish
tool-call mimicry, reasoning-only turns, and truncation.
Validated live by driving the production `RunAdvisor` path against
`claude-sonnet-4-6` through the dev.coder.com AI gateway: the failing
scenario went from 3/3 errors to 6/6 genuine advice (with and without
extended thinking), with the control scenario unaffected.
Related: CODAGT-593 (per-step advisor runtime recreation, addressed
separately) and CODAGT-742 (advisor tool call design).
<details>
<summary>Investigation and validation details</summary>
### Reproduction
A CLI prototype constructed the exact conversation snapshot the
generation preparer hands the advisor tool and called the real
`chatadvisor.NewRuntime` / `Runtime.RunAdvisor` / `BuildAdvisorMessages`
/ `chatloop.GenerateAssistant` chain against live `claude-sonnet-4-6`,
with a stream-teeing model wrapper capturing what `runner.go` discards
(finish reason, part kinds, reasoning text).
| Scenario (snapshot contents) | Thinking | Before fix | After fix |
|---|---|---|---|
| control: call #1 state, no prior advisor exchange | on | 3/3 advice |
2/2 advice |
| repro: call #2 state, prior advisor `tool_use`/`tool_result` pair
forwarded | on | 3/3 `advisor produced no text output` | 3/3 genuine
advice |
| repro | off | 2/3 same error, 1/3 degenerate advice ("I'll ask the
advisor...") | 3/3 genuine advice |
Every failing response was a tiny thinking block, zero text, zero
tool-call stream parts, finish reason `stop`; the model's own reasoning
text showed it deciding to "make the second tool call" in a request with
`tools=0`. The refunded `remaining_uses: 1200` in the failing
tool-result JSON matches the original issue screenshot.
### Decision log
- Tool exchanges are folded into a single user-role note per call/result
pair. A first attempt rendered assistant-authored `[tool call:
name(input)]` text lines plus separate result messages; live runs then
returned the literal `[tool call: advisor(...)]` line as the advice 6/6
times. The bare assistant call line is itself an imitable pattern, so no
assistant-authored tool artifact may survive the handoff. The folded
user-role note produced 6/6 genuine advice.
- An assistant message that carried only tool calls is dropped entirely;
the folded notes preserve the information.
- `dropOrphanToolMessages` was removed: without raw tool blocks there is
no provider pairing constraint, and an orphaned result note retains
context value.
- A reasoning-budget-starvation hypothesis (thinking budget consuming
`MaxOutputTokens`) did not reproduce on `claude-sonnet-4-6`; the model
adapts thinking length to the cap. The enriched error would identify
such cases on other models via `finish_reason=length`.
- CODAGT-593 (persisting the advisor runtime across steps) is
intentionally not addressed here; it shrinks the priming window but the
handoff fix is what removes the failure mode.
</details>
---
*This PR was generated by Coder Agents on behalf of @ThomasK33 (Linear
agent session for CODAGT-592).*
|
||
|
|
3227cac217 |
feat: add manual chat compaction via /compact (#27081)
Adds a user-triggered `/compact` action for Coder Agents chats: typing
`/compact` in the composer (or picking it from the `/` trigger menu)
summarizes the conversation so far to free up context window space.
## How it works
- New `POST /api/experimental/chats/{chat}/compact` endpoint
(owner-only, RBAC `ActionUpdate`, excluded from the public API reference
via `x-apidocgen skip`). It marks the chat with a durable one-shot
`chats.compaction_requested_at` signal and moves it `waiting -> running`
via a new `RequestCompaction` state transition; no message row is
inserted. AI Gateway attribution needs no per-request key: generation
preparation resolves the owner's synthetic API key (#27170) like any
other turn.
- `RequestCompaction` hands off chat ownership (clears
`worker_id`/`runner_id`) so a worker acquisition hint is published;
since the transition changes no history, the previous runner could
otherwise miss the request under reordered pubsub delivery.
- The background chat worker picks the chat up like any other turn. A
pending manual request takes precedence over turn completion in the
generation decision, and forces compaction even below the automatic
threshold (and when compaction is disabled via threshold=100). The
commit step consumes the request marker in the same transaction; any
transition that ends the turn clears stale markers.
- The summary triplet reuses the automatic-compaction path, now tagged
with a `source` (`automatic` | `manual`) that is plumbed through
streamed progress parts, persisted tool JSON, and the UI label
("Summarized (manual)").
- Validation order: busy chats reject with 409 (state-machine conflict),
empty/already-compacted chats with 409 "nothing to compact", archived
chats with 400; the owner usage-limit check runs last so no-op requests
surface the specific conflict instead of a limit error.
- Web UI: the `/` trigger menu now has a built-in "Commands" group
listing `/compact`; submit intercepts exactly `/compact` and calls the
endpoint instead of sending a message. A personal or workspace skill
named `compact` takes precedence over the built-in command; while skill
collisions are still resolving, an exact `/compact` submission is
blocked with a retryable hint instead of leaking as message text.
History and queued-message edits are never intercepted. After
compaction, the context usage indicator resets to its unknown state
until the next assistant response reports fresh usage, instead of
showing the stale pre-compaction number.
- codersdk: `ExperimentalClient.CompactChat`.
Worker-path execution (rather than compacting synchronously in the
handler) reuses the existing lock fencing, live "Summarizing..."
streaming, retry accounting, restart resilience, and debug-run
observability. Rationale documented in `coderd/x/chatd/ARCHITECTURE.md`.
## Testing
- State machine: transition-matrix coverage for `RequestCompaction`,
marker lifecycle tests (carried by lease renewals/queue appends, cleared
by terminal transitions, consumed by commit), ownership handoff +
acquisition hint assertions.
- Worker: decision-ordering and forced-compaction unit tests;
active-server end-to-end test (manual compact below threshold produces a
`source=manual` summary, returns to `waiting`, no assistant follow-up;
busy chat rejected).
- API: success, archived, non-owner, RBAC-denied, empty-chat, no-daemon
cases; usage-limit ordering (at-limit owners still get
state/nothing-to-compact conflicts for no-op requests, with marker
rollback).
- Frontend: Storybook play tests for the Commands menu group, submit
intercept, skill-name collision, queued-edit passthrough, and
manual/automatic tool rendering; unit tests for command availability
resolution and the post-compaction context usage reset.
> This PR was created by Mux, an AI coding agent, working on Mike's
behalf.
|
||
|
|
4ed6fcced7 |
refactor(coderd): stop storing chat gateway key IDs and drop the columns (#27171)
> Mux is working on behalf of Mike. ## Summary Stop reading and writing the legacy `api_key_id` columns on chat messages and queued messages, and drop the columns in the same PR. Runtime AI Gateway attribution continues to use the per-user synthetic key introduced by #27170. With the columns gone, `sqlc` generates `database.ChatMessage` and `database.ChatQueuedMessage` without `api_key_id`, so no transitional query scaffolding is needed. Migration `000548` drops the `api_key_id` columns. #27170 already removed their foreign keys, so the down migration re-adds nullable text columns without constraints. Previous column values cannot be restored. Also moves the model config validation in `CreateChat` above the message-building work so a disabled or invalid model fails fast. On main this mattered more: the old ordering minted a synthetic API key before rejecting the request. Deploy note: replicas still running the previous release write `api_key_id` on insert, so chat message inserts on old replicas fail during the rolling window after the column drop. This was previously split across two PRs to avoid that window; per review feedback the split added more churn than it was worth for an experimental surface. Depends on #27170 (merged). |
||
|
|
b511a68ab0 |
fix(coderd/x/chatd): clarify wait agent timeout (#27335)
The model-visible timeout schema did not state its five-minute default. Clarify that waits return on completion and that a timeout leaves the agent running. |
||
|
|
9f4ddea571 |
feat: revoke MCP server OAuth grants at the provider on disconnect (#27300)
Closes [CODAGT-805](https://linear.app/codercom/issue/CODAGT-805/revoke-oauth-grants-at-the-source-for-mcp-servers). The experimental MCP server OAuth2 disconnect endpoint previously deleted only the stored token row, leaving the grant active at the OAuth provider. This PR adds provider-side token revocation while keeping local disconnect independent of provider availability. ## Changes - Add `mcp_server_configs.oauth2_revocation_url` in migration `000547`. The value can be configured manually, discovered from RFC 8414 metadata, and managed through the MCP server settings UI. Non-admin responses redact it with the other OAuth2 fields. - Revoke the refresh token first through the RFC 7009 endpoint, then fall back to the access token only for `unsupported_token_type`. Public clients send `client_id`; confidential clients use `client_secret_basic`. - Delete the local token transactionally before best-effort provider revocation. Callers without a token receive the same response for hidden and nonexistent config IDs, and provider failures return a generic warning without exposing provider response bodies. - Require HTTPS revocation endpoints except for HTTP loopback URLs. Redirects must preserve the POST and remain on the configured origin. Redirect errors omit provider-controlled paths and query strings so reflected token material cannot enter logs. - Treat `200 OK` and `204 No Content` as completed revocations. `202 Accepted` remains a failure because it does not confirm completion. - Prevent an in-flight refresh from recreating a token deleted by disconnect. Refresh persistence now uses an optimistic update keyed by token ID and `updated_at`; only the OAuth callback can create a token row. Refresh conflicts reload the current row or clear in-memory auth when disconnect deleted it. - Return `{token_revoked, token_revocation_error}` from disconnect, while retaining SDK compatibility with the legacy `204` response. The UI surfaces provider revocation failures as warning toasts. - Document revocation endpoint discovery, HTTPS requirements, and best-effort disconnect behavior. No token or no configured revocation URL returns `token_revoked: false` without an error, so disconnect remains idempotent. > Updated by Mux, an AI coding agent, on Mike's behalf. |
||
|
|
33fcc9de68 |
fix(coderd/x/chatd): drop stale APIKeyID from CreateOptions test literal (#27331)
> Mux is working on behalf of Mike. Closes coder/internal#1622 (ENG-3061). ## Problem `main` is broken: the chatd test package fails to compile, taking down `lint`, `test-go-pg`, `test-go-pg-17`, and `test-go-race-pg`. This was a semantic merge conflict between two individually green PRs: - #27170 removed `APIKeyID` from `chatd.CreateOptions` (chatd now mints a synthetic gateway key from the chat owner). - #27070 branched before that merge and added the `CreateChatProviderDisabledRejected` test, which sets `APIKeyID` in a `CreateOptions` literal. Its CI ran against the old base and passed. Merged together: `unknown field APIKeyID in struct literal of type CreateOptions`. ## Fix Two lines in the test: - Drop the stale `APIKeyID` field from the `CreateOptions` literal. - Create the chat owner with `dbgen.User` instead of a random `uuid.New()`. On current `main`, `CreateChat` resolves the owner's synthetic API key before the model-config recheck, so a nonexistent owner fails with `sql: no rows` instead of reaching the `ErrInvalidModelConfigID` assertion. ## Validation - `go build ./...` - `go test ./coderd/x/chatd/...` (full package, passes) - pre-commit hooks (lint/go, lint/ts) |
||
|
|
9b3af629cd | fix: hide and reject chat models from disabled AI providers (#27070) | ||
|
|
5f5efa49da |
fix: stop sending adaptive thinking to pre-4.6 Anthropic models (#27314)
## Problem
A chat model config with `reasoning_effort` set on a pre-4.6 Anthropic
model (for example `claude-haiku-4-5`) fails every generation with HTTP
400 `adaptive thinking is not supported on this model`, surfaced in chat
as "Anthropic returned an unexpected error." The fantasy Anthropic
provider always serialized effort as `thinking: {type: "adaptive"}` plus
`output_config.effort`, a shape only Claude 4.6+ accepts.
## Changes
- Bump the coder/fantasy pin to include coder/fantasy#47: the provider
now converts effort into `{type: "enabled", budget_tokens}` on models
older than Claude 4.6, with the budget derived from the call's
`max_tokens` (aibridge-mirroring ratios, 1024-token API floor; below the
floor thinking is omitted, which keeps small-budget calls like title
generation working). Adaptive-capable models keep the current shape, and
Opus 4.5 keeps `output_config.effort` alongside the derived budget since
it supports effort without adaptive thinking. Models older than Claude
3.7 predate extended thinking, so effort sends no thinking at all there.
`minimal` is normalized to `low`, `xhigh` falls back to `max` on
adaptive models that predate the xhigh tier (Claude 4.7+); effort `none`
disables thinking, including an explicit `thinking: {type: "disabled"}`
on Claude 5+ models that otherwise run adaptive thinking by default. The
Bedrock provider wraps the Anthropic one, so both are covered, and
Vertex-style `@date` model IDs parse correctly.
- `TestActiveServer_CompactionModelOverride` previously codified the
buggy shape (asserting `output_config.effort` sent to
`claude-3-5-haiku-latest`). The summary-routing subtest is now a
three-case table: pre-thinking override models (Claude 3.5) expect no
thinking, legacy budget-thinking ones (Haiku 4.5) expect enabled
thinking with the derived budget, adaptive-capable ones still expect
`output_config.effort`.
- New regression test `TestActiveServer_AnthropicModelReasoningEffort`:
a `claude-haiku-4-5` config with `reasoning_effort` produces enabled
thinking with the derived budget and no `output_config` on the wire, and
a `claude-sonnet-5` config with effort `none` sends an explicit thinking
disable.
- `chattest.AnthropicRequest` gains a `Thinking` field so tests can
assert the thinking config.
- One-sentence note in the chatd ARCHITECTURE reasoning-effort section.
No chatd production code changes: `ApplyReasoningEffort` keeps setting
`Effort`, which is now valid for every Anthropic model.
## Validation
- `go test ./coderd/x/chatd/...` passes (19 packages).
- Fork PR validated separately: full fantasy test suite plus new
provider unit tests (version gating incl. Vertex/Bedrock IDs, budget
derivation, floor behavior, normalization, effort `none` incl. Claude 5+
disable, Opus 4.5 effort preservation, sampling-param stripping),
golangci-lint clean.
Closes
[CODAGT-812](https://linear.app/codercom/issue/CODAGT-812/reasoning-effort-on-pre-46-anthropic-models-fails-generations-with).
> This PR was authored by Mux, an AI coding agent, acting on Mike's
behalf.
|
||
|
|
46d1823c0a |
feat: add workspace skills to agent chat slash menu (#25600)
> This Pull Request was updated by Mux working on behalf of Mike. Adds workspace skills to the agent chat slash menu, sourced entirely from the chat's pinned context resources (the single-chat GET response the page already fetches), the same inventory `read_skill` resolves from. No new API endpoint is introduced. Personal entries insert `/name`, or `/personal/name` when the name collides with a workspace skill or the chat's pinned context has not resolved yet; workspace entries insert `/workspace/name`. Qualified aliases stay searchable even when the displayed trigger is bare. Before a chat binds a workspace (new chat form, or a selected but unbound workspace), the menu lists personal skills only. Sending a message invalidates the chat detail query, and chatd broadcasts a context watch event when a first-turn bind pins the chat, so the menu picks up newly pinned context without a reload. Makes `UpdateChatWorkspaceBinding` a no-op when the requested workspace/build/agent binding is unchanged, preserving `updated_at` so chat list ordering and watch events stay stable. Includes regression coverage for the no-op binding guard, pinned-context skill mapping, collision qualification, and skills menu behavior. Refs [CODAGT-474](https://linear.app/codercom/issue/CODAGT-474/ux-improvements-for-coder-agents) (skills autocompleting in the editor). |
||
|
|
997b5d0843 |
feat: add synthetic gateway keys (#27170)
> Mux is working on behalf of Mike. ## Summary Add a per-user synthetic API key for chatd AI Gateway attribution. Chatd resolves the key from the chat owner, extends it before expiry, and discards the generated bearer token so the key is never a usable credential. There is no mapping table. The key is resolved from `api_keys` by a deterministic token name (`chatd_<owner_id>_session_token`), mirroring the provisionerd session token model, with three deltas that chatd needs: - **Login type guard**: token names are unvalidated user input, so a user can create a bearer token with the colliding name. The lookup excludes `login_type = 'token'` rows, so chatd never picks up (or extends) a real user token. Synthetic keys are minted with the owner's login type, which is never `token`. - **In-place expiry extension instead of delete-and-reinsert**: chat generations have no stop boundary, and an in-flight generation may have already delegated the current key ID to aibridged. Extending `expires_at` keeps the key ID stable forever. - **Advisory-lock mint**: the unique index on token names is partial (`WHERE login_type = 'token'`), so nothing DB-enforces uniqueness for synthetic keys. A per-user advisory lock serializes concurrent mints. Keys carry a minimal scope (`api_key:read`) as defense in depth; the delegated gateway path never evaluates scopes and the secret is discarded at mint. Migration 000544 removes the foreign keys from the legacy message and queue `api_key_id` columns while chatd continues stamping them for rolling compatibility. Stale IDs are tolerated because routing uses `chats.owner_id`. Individual key deletion, delete-all, and password reset remove the key without changing chat history or queue versions, and the next lookup remints it. Suspension does not delete the key; delegated gateway authorization rejects inactive users at request time. This is the first PR in a three-PR rollout and must be fully deployed before #27171. Refs https://linear.app/codercom/issue/CODAGT-561/maintain-synthetic-api-key-per-user-per-chat |
||
|
|
1ac106255b | feat: add Anthropic 1M context window toggle for Agents model configs (#27257) | ||
|
|
f7481c5d08 |
feat: Add full text search over chat messages (#27126)
Closes CODAGT-721 Closes CODAGT-722 Closes CODAGT-723 Closes CODAGT-724 Closes CODAGT-725 This PR adds the database and API pieces necessary to support full-text chat message search. - Adds required chat schema for full-text search - Adds dbpurge job to populate search_tsv in the background - Adds `search` parameter to GetChats query - Adds `search` filter to `searchquery.Chats` - Wires chat search filter into chats API > Implemented by Coder Agents, reviewed and tested by a human. |
||
|
|
e489092154 |
feat: handle revoked OAuth grants for MCP servers gracefully (#27264)
Closes [CODAGT-792](https://linear.app/codercom/issue/CODAGT-792/handle-revoked-oauth-grants-for-mcp-servers-gracefully). When a user revokes an upstream OAuth grant for an MCP server used by Coder Agents, Coder kept treating the cached token as valid: `invalid_grant` refresh failures were logged and swallowed, the dead bearer token kept being attached, the list endpoints re-attempted the refresh on every call, and the UI kept showing the server as authenticated. ## Changes Backend, mirroring the `external_auth_links` prior art: - New migration adds `mcp_server_user_tokens.oauth_refresh_failure_reason`. `UpsertMCPServerUserToken` clears it, so completing the OAuth flow again recovers the row. - New `MarkMCPServerUserTokenRefreshFailure` query records the failure and clears all token material, guarded by an `updated_at` optimistic lock so a stale failure never clobbers a concurrently refreshed token (on a lock miss the winner's row is used). - `mcpclient.IsPermanentRefreshError` classifies `*oauth2.RetrieveError` codes: only `invalid_grant` and `bad_refresh_token` are permanent. Client/config errors (`invalid_client`, `unauthorized_client`, ...) stay transient for the user row since reconnecting cannot fix them. - chatd token refresh and the MCP list/get endpoints persist permanent failures, return cleared tokens for the in-flight request, and skip provider calls for already-failed rows. - `buildAuthHeaders` no longer attaches an Authorization header for failed tokens, so chat degrades by omitting that server's tools instead of sending a dead bearer. API and UI: - No new API surface. A permanently failed token simply reports `auth_connected: false`, so the existing "Auth" button and "Not authenticated" tooltip appear and the user re-runs the same OAuth flow to recover. An earlier revision added an `auth_status` enum (`connected` / `not_connected` / `reconnect_required`) with a dedicated "Reconnect" button; it was collapsed to keep the API minimal since both states lead to the identical re-auth action. Out of scope (follow-up): typed 401-on-connect detection and forced refresh. mcp-go exposes no stable typed 401 signal in the static-header path, so a revocation while the access token still looks valid locally stays undetected until expiry triggers a refresh. ## Testing - Unit and integration tests: classifier, chatd refresh paths (permanent/transient/race/persist-failure), API endpoints (revoked, transient, no-retry caching, re-auth recovery, stale-lock), dbauthz, dbcrypt, migrations. - Dogfood UAT against a dev instance with a mock IdP returning `invalid_grant`: revoked grant detected on refresh and persisted once (no repeated IdP calls), chat with the revoked server selected completes with the server's tools omitted, and re-auth restores the connected state. > This PR was authored by Mux, working on Mike's behalf. |
||
|
|
f997afa220 |
feat(coderd/x/chatd/chatloop): retain user constraints in compaction summaries (#27230)
Compaction summaries drop or soften user-stated constraints, corrections, and prohibitions, so post-compaction assistants repeat behavior the user already corrected. Add a summary prompt bullet that instructs the summarizer to quote them, treat them as standing until revoked, and attribute rules to their true source instead of defaulting to the user. Validated offline on unseen human chats: holdout P1 delta +0.175 (arbitrated), 13/18 cases improve. This improves per-compaction retention only; it does not address deep-chain correction loss. |
||
|
|
21d08241e9 |
fix(coderd/x/chatd): recover timed out agents (#27254)
Closes CODAGT-802 Coder Agents only escalated failed workspace dials when the agent had connected and later disconnected. An agent that never connected and had already exceeded its `connection_timeout` stayed on the soft retry error indefinitely, so a chat could keep attempting tools against an unhealthy workspace. To fix, we'll classify the latest agent after a failed dial and return stop/start recovery guidance when its status is `timeout`. Agents still connecting, including templates with `connection_timeout = 0`, keep the existing retryable behaviour. ## Before <img width="843" height="229" alt="image" src="https://github.com/user-attachments/assets/d659a376-c8d4-4983-b7d7-d1a699770dfb" /> ## After <img width="848" height="250" alt="image" src="https://github.com/user-attachments/assets/26b88f72-f67c-4d9e-87b1-51d701b7352f" /> |
||
|
|
cc11c8a536 |
feat: surface model content-filter refusals as a blocked chat error (#27118)
Blocked turns from a provider's content filter (Anthropic's `refusal` stop reason with empty content) previously ended silently on the "Thinking" spinner. They now end as a terminal `content_filter` error that renders as a "Response blocked" message with the provider's category and explanation. <img width="888" height="335" alt="image" src="https://github.com/user-attachments/assets/cef85a59-4091-4e62-9d45-1eb06748db48" /> Closes CODAGT-611 Follow-ups will involve implementing fallbacks, but this alone is pretty important |
||
|
|
d99ed85db8 | fix: fix test flake in nats subscribe cleanup (#27238) | ||
|
|
a567f6a89f | feat: allow admins to override the chat compaction model (#27151) | ||
|
|
0f55c283f1 | fix: use backend-selected chat agent for desktop, git, terminal (#26959) | ||
|
|
55b06f14a3 | feat: allow overriding advisor reasoning effort (#27196) | ||
|
|
6f6d7539c8 |
feat: remove unused chat statuses pending, paused, and completed (#27064)
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. |
||
|
|
ad29777cb2 | feat: NATS mTLS pubsub implementation (#26902) | ||
|
|
1497ba14fe |
refactor: type computer use provider as an enum (#27086)
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. |