Skip to content

feat: expose structured output request IDs and results on chat messages - #29987

Draft
ThomasK33 wants to merge 2 commits into
thomask33/structured-output-lifecycle-gapsfrom
thomask33/structured-output-api-projection
Draft

ThomasK33 wants to merge 2 commits into
thomask33/structured-output-lifecycle-gapsfrom
thomask33/structured-output-api-projection

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Exposes structured output request IDs and results on chat messages. It is the api-projection slice (C1) of the structured-output backend roadmap and is stacked on #29986. Ordinary messages serialize exactly as before.

  • Request ID on user and queued messages. ChatMessage and ChatQueuedMessage gain an optional structured_output_request_id. It is set on a user message, or a queued message, whose content holds exactly one valid structured output request part; promotion keeps it because the part moves with the message.
  • Result on receipts. ChatMessage gains an optional structured_output (ChatStructuredOutput). It is set only on a receipt row: an assistant message visible only to users whose content is a text fallback plus exactly one valid outcome part. The value passes through as raw JSON, so a succeeded null stays an explicit null.
  • Content unchanged, metadata fails soft. Internal parts stay hidden and a receipt keeps its text fallback in content. Malformed or ambiguous metadata leaves the new fields unset and never drops the message.
  • One conversion for every surface. The shared db2sdk.ChatMessage and db2sdk.ChatQueuedMessage conversions parse each message once and fill the fields, so REST, the stream snapshot, message events, history resets, queue updates and send and edit responses stay identical.
  • TypeScript type of the output value (deferred from feat: add internal structured output metadata codecs #29945). The generated TypeScript type for ChatStructuredOutput.value was the global raw JSON mapping Record<string, string>, which is wrong for a value that can be any JSON. scripts/apitypings renames AgentHookRawMessages to RawMessageFieldsAsUnknown and adds ChatStructuredOutput.value to its field list, so the value is generated as unknown. This resolves the TypeScript finding deferred from the A3a PR.

Nothing here makes structured output reachable through the API yet; activation comes later in the stack. No migration, query or frontend component change. Generated by make gen: coderd/apidoc/docs.go, coderd/apidoc/swagger.json, docs/reference/api/chats.md, docs/reference/api/schemas.md and site/src/api/typesGenerated.ts (668 lines). Handwritten: 328 lines, 239 of them tests.

The duplicate tool call ID issue described in #29975 is fixed by the next slice in this stack, before the activation slices. Other deferred follow-ups are tracked in #29982.

Validation:

  • go test and -race for coderd/database/db2sdk, codersdk and coderd/x/chatd, plus go test ./coderd -run Chat.
  • make gen drift check (no diff) and make pre-commit, which includes lint/ts over the generated types.
  • The make pre-push hook on push (full make test, test-js, site build).
  • Remote dogfood UAT on this exact commit: native package gates (every chatstate test, the whole chatd package, the coderd chat message tests including TestChatMessagesProjectStructuredOutput, the db2sdk and codersdk tests including TestChatMessage_StructuredOutput), -race, fuzzing, a strace run with one loopback connection, a generated-files check that reran apidocgen, the docs formatters and apitypings, compared every generated file with the committed one and typechecked the frontend (ChatStructuredOutput.value is unknown), an independent probe plain and with -race, an end-to-end projection check on a real deployment with a scripted fake provider (raw REST JSON, the live stream and a fresh snapshot agree for a request, succeeded and failed receipts, a queued request ID with its queue_update, a queue deletion receipt and a promoted request ID; ordinary messages unchanged; a live model chat matched across REST and the stream), and an ordinary live-model chat smoke with recorded video. All required checks passed in round 1, with no known limitation reproduced. Screenshots and video are in the first comment.

📋 Implementation Plan

Coder Agents structured output: backend implementation plan

1. Outcome and scope

Add a per-request response_format to the Coder chat API. An agent can use its normal tools, then submit a JSON value. Coder validates that value and publishes a durable, request-correlated success, failure, or cancellation. The later AI SDK adapter reads this result without implementing a finalizer tool or parsing tool names.

This plan replaces the previous plan. It targets the inspected checkout at 06d421fbcf2c on 2026-09-22, matching the locally available origin/main at inspection. The working tree was clean. Recheck the implementation base before starting; remote freshness was not established by a fetch.

Scope: Go server, Go SDK contracts, generated API artifacts, API documentation, and tests. Generated site/src/api/typesGenerated.ts changes are allowed. No handwritten TypeScript, external coder/ai-sdk changes, UI feature, provider-library upgrade, new endpoint, or new database resource.

Guarantee: a successful result contains exactly one server-validated JSON value. This is not a promise of provider-native constrained decoding, factual correctness, or guaranteed model compliance. A model that will not comply produces a bounded failure, never successful plain text.

Initial limitations: inline Draft-07 JSON schemas; no references or custom vocabularies; no partial structured-value streaming; no structured-output contract on plan-mode or specialized child-mode turns. Normal root chats may still use subagents, dynamic tools, MCP tools, and provider tools. The parent's format is not inherited by subagents. Existing authorization and organization scope remain unchanged.

2. Current evidence and corrections

Verified seam Consequence for this plan
coderd/chat_routes.go:15-98 mounts create, messages, edit, queue, interrupt, and stream under /api/v2/chats. Do not use the old experimental routes for these operations. There is no new route to introduce.
codersdk/chats.go:568-683 defines create, send, edit, and their response batches. Cover edit semantics as well as create/send. A queued send is not yet a generated result.
go.mod:111 replaces Fantasy with github.com/coder/fantasy at 9a3598480a71. fantasy.Call has tools but no output schema; ObjectCall has a schema but no tools. Keep the existing tool-capable call path. Do not switch the agent loop to StreamObject.
chatloop/tool_definitions.go:13-47 builds a root object from ToolInfo.Parameters, then mutates nested maps through schema.Normalize. A caller schema needs a preserved full definition, not lossy flattening or shared mutable schema maps.
chatloop/chatloop.go executes local tools and truncates their ordinary text results; chatprompt.go:765-780 stores valid JSON directly, otherwise wraps text. Do not use ordinary finalizer result text as the authoritative output.
generation.go:562-588 loads visible durable history, including compressed rows. Prompt SQL at queries/chats.sql:496-552 drops older compressed input. message_conversion.go:461-475 also skips compressed rows. Derive structured-request state from durable history, not the provider prompt or the current uncompressed-user helper.
message_conversion.go:307-382 writes model-only compaction summaries and copies pending user content into model-only rows. Preserve the original request identity across compaction. A copied or hook-generated user row is not a new request.
generation.go:1427-1510 runs stop hooks before finishing. A hook can keep the turn running. A validated finalizer call is only a candidate. Publish success only after stop-hook acceptance.
chatstate/transitions.go:602-711 replaces edited input with a new row and deletes its suffix and queue. FinishTurn, FinishError, and FinishInterruption own terminal transitions. Define supersession and cancellation in those transactions. Do not infer completion from chat-level waiting or last_error.
db2sdk.go:1740-1755 strips internal hook-context parts. Frontend parsers exhaustively switch on public part variants. Keep new storage parts internal; project additive message fields. No new public content variant or handwritten frontend change.
database/dump.sql:1510-1551 advances execution history on any message insert. runner.go:159-173 fences active work on history/status, not snapshot changes. A genuine terminal receipt needs a narrow insert-trigger exception to avoid interrupting unrelated generation.
stream_loop.go:114-164,226-291 gates message deltas on history but stores a snapshot watermark. chatstate/machine.go:254-273 reads under a chat-row share lock. Separate message-revision delivery from execution changes before exempting receipt inserts. Keep the existing read boundary and preview fencing.
The former chatopenai/responses.go and chatd chain-ID symbols are absent from this checkout. Remove the old chain-mode implementation work. Still test ordinary provider transcript replay.

The previous plan also left validator choice, strict:false, result shape, limits, and delivery boundaries undecided. Those are resolved below.

3. Public API contract

3.1 Requests

Add optional ResponseFormat *ChatResponseFormat to CreateChatRequest, CreateChatMessageRequest, and EditChatMessageRequest.

{
  "content": [{"type": "text", "text": "Inspect the project and report its test command."}],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "project_report",
      "schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
        "additionalProperties": false
      }
    }
  }
}
  1. Omitted or JSON null format means ordinary text behavior on create/send. Explicit {"type":"text"} is also accepted. Reject an empty format object, unknown type, or json_schema accompanying text.
  2. json_schema requires name and schema. Name matches ^[A-Za-z0-9_-]{1,64}$. Optional description is at most 1024 UTF-8 bytes. Reject unknown fields inside the format envelope, including strict. Validation is always enabled; there is no relaxed mode.
  3. The schema document is an object. The output it describes can be an object, array, scalar, or JSON null. Do not impose an object-only output restriction.
  4. Edit omission or null preserves the edited message's format; explicit text clears it; explicit json_schema replaces it. Read and validate preserved metadata under the same history guard as the edit. The replacement request receives a fresh request ID.
  5. Validate before persistence, queue insertion, hook dispatch, interrupting existing work, or changing chat settings. Repeat only the cheap state-dependent checks inside the transaction after any await.
  6. Reject a structured request with an effective plan mode or specialized child mode. Do not silently accept an incompatible mode. A later incompatible mode switch must not silently remove an active or queued output obligation.
  7. HTTP validation errors use existing codersdk.Response.Validations with the exact response_format... field and HTTP 400. Existing authorization, unavailable-model, conflict, and body-limit statuses keep their meanings.

Admission gate: add default-off chat-structured-output experiment (ExperimentChatStructuredOutput) with no development bypass. Gate only acceptance of new formatted create/send/edit requests. When disabled, a syntactically supplied schema request, including an edit preserving a schema, returns HTTP 400 with a response_format validation error explaining that the feature is disabled. Explicit text/ordinary requests remain available. Persisted accepted work is still processed when the experiment is disabled. Enable acceptance only after all serving and worker replicas are compatible; drain or cancel accepted work before downgrading. Do not use the experiment to disable recovery or hide persisted results.

3.2 Correlation and terminal results

Mint a server-generated UUID once when accepting a structured request. Persist it with the format. Expose optional structured_output_request_id on ChatMessage for the original input and on ChatQueuedMessage. Preserve it through promotion, retries, and compaction copies. Create-chat callers obtain it from the first committed user message via existing message retrieval or streaming; no extra create endpoint is needed.

Add optional ChatMessage.StructuredOutput *ChatStructuredOutput:

{
  "id": 901,
  "role": "assistant",
  "content": [{"type":"text","text":"{\"command\":\"make test\"}"}],
  "structured_output": {
    "request_id": "11111111-1111-4111-8111-111111111111",
    "status": "succeeded",
    "value": {"command":"make test"}
  }
}

The receipt is an ordinary committed message with an additive field. Its assistant role is a transcript convention, not a provider response. It is visible to clients but excluded from provider prompts.

Status Required payload Meaning
succeeded value, including explicit JSON null Validation succeeded and turn termination was accepted.
failed error: {code, message} No usable output. Codes: not_produced, validation_exhausted, generation_failed, configuration_error.
cancelled error: {code, message} Codes: interrupted, superseded, queue_deleted. No value.

Use json.RawMessage for value, not float64 decoding/re-encoding. Preserve numeric value and exact string data; JSONB may normalize whitespace/key order, so do not promise byte-identical JSON. Do not call the result RFC-canonical JSON.

Only committed receipt messages carry structured_output. Candidate tool arguments, tool acknowledgments, message_part deltas, and chat status events are not results. Existing message replay/reset/deletion behavior remains authoritative. Clients upsert by message ID and match by request UUID. A successful receipt can arrive while the next queued turn is already running.

Failure/cancellation receipts have a short text fallback. A success receipt has JSON text as its fallback. Existing clients can display the outcome without recognizing the new field. Whole-chat deletion and retention keep existing resource-deletion semantics; neither is a permanent result archive.

4. Schema validation and resource limits

Use the already-present github.com/xeipuuv/gojsonschema v1.2.0, promoted from indirect to direct without a version upgrade. Pin Draft7 and disable dialect auto-detection. Its byte loaders use json.Number; numeric validation uses rational arithmetic. Do not use the limited Fantasy schema struct as a validator.

Keep trusted meta-validation separate from user-schema compilation. Compile only the fixed bundled Draft-07 meta-schema in a fresh trusted loader, validate the parsed user schema as data, then compile it in a different loader with meta-validation already satisfied and a deny-all document loader factory. SchemaLoader.Validate=true recursively invokes Compile(NewReferenceLoader(...)) and changes loader state, so do not assume a supplied factory controls that nested call. Prove the selected two-stage path offline and with network/filesystem traps before importing it into ingress.

The alternative google/jsonschema-go v0.4.3 is already direct and denies remote loads by default, but its current resolver does not fully meta-validate schemas and stores numeric bounds as float64. Avoid adding a second handwritten schema validator to compensate.

  1. Omitted $schema means Draft-07. Accept only the canonical Draft-07 identifier when supplied. Reject other dialects rather than silently reinterpret them.
  2. V1 accepts inline schemas. Reject $ref, $id, legacy id, $defs, definitions, and custom vocabulary keywords at schema positions. Do not reject an ordinary property or string value merely because it contains those names.
  3. Allow standard inline Draft-07 assertions, including unions, object/array constraints, enums, anyOf, oneOf, allOf, and fractional numeric bounds. Reject unknown schema keywords. Treat annotations such as title/description/default as annotations; do not apply defaults or coerce values.
  4. Supported format names use the pinned library's built-in checks. Reject unrecognized names instead of silently skipping them. Document this finite list and that pattern uses Go regexp syntax. Do not mutate process-global format registries. regex is not in the supported list: the library compiles every instance string, and counted repetitions make that cost not adequately constrained by the selected resource budget.
  5. Meta-validation uses the dependency's bundled Draft-07 meta-schema. Supply a deny-all document loader factory for user-controlled resolution. Compilation and validation must make no HTTP, DNS, or filesystem reads. Do not set a global HTTP client or rely only on $ref rejection.
  6. Reject duplicate JSON object keys, trailing JSON, invalid UTF-8, unpaired surrogate escapes, decoded U+0000, non-finite numbers, and oversized/deep values before compilation or instance validation. The Unicode restrictions are required for PostgreSQL JSONB storage; document them. Decode numbers with UseNumber; preserve RawMessage for storage. Apply the same checks to schemas, format strings, model values, and recovered metadata.
  7. The resource scanner must distinguish schema positions from literal data. Test property names and const/enum/default objects containing $ref, $id, and the trusted meta-schema URI. Those values must neither trigger loading nor shadow the trusted compiler's state.

Initial server-side caps, fixed rather than caller-configurable:

Input Limit
Schema document 16 KiB; 16 structural levels; 256 schema nodes
Branches 8 total: each allOf/anyOf/oneOf element, each not/if/then/else subschema, and each schema-form dependencies value; at most 4 nested branch levels
Pattern (pattern values and patternProperties keys) 256 bytes and 128 compiled regexp instructions each; a saturating size estimate above 1024 is rejected before compiling
patternProperties 4 patterns per schema; outputs for such schemas are limited to 256 JSON nodes
Finalizer arguments 80 KiB before decoding
Unwrapped output 64 KiB; depth 32; 4096 JSON nodes; 256 elements per array
Numeric literal 128 bytes; absolute exponent at most 1024
Model-facing validation feedback At most 4 errors and 1024 bytes, paths and rules only

Exploratory measurements against gojsonschema v1.2.0 set the branch, pattern, and patternProperties limits above (cumulative allocation, not peak heap, on the development host). The library recompiles each patternProperties pattern for every key on every validation: 2 small patterns over 4095 keys took 0.26 s and allocated 192 MiB. It keeps every validation error, about 1 KiB each, without fail-fast: 32 failing allOf branches over 4000 keys produced 128k errors and 131 MiB, and 120 schema-form dependencies over 4000 keys produced 480k errors and 465 MiB, while 8 branches over 4095 nodes produced 34 MiB in 0.10 s. The schema-position preflight enforces these limits before any library call. The compiler's own resource qualification on the implemented code remains an A1c gate.

Enforce limits before expensive operations and on recovered persisted metadata. Use errors for untrusted input, never panics. Assert programmer-owned invariants in tests and return explicit internal errors in production. Add boundary tests, bounded fuzzing, offline loader tests, and worst-case benchmarks. A timeout goroutine around non-cancellable validation is not a resource bound. If qualification shows excessive CPU or memory use at these caps, lower the caps before API activation and update tests/docs together.

5. Durable state and execution design

5.1 Storage and state reconstruction

Use existing JSONB message content, following internal hook-context conventions. Add internal-only request, candidate/control, and outcome metadata shapes. Name the dedicated terminal discriminator structured-output-outcome; do not reuse it for continuation/control records. Exclude these types from public generated unions, explicitly strip them in db2sdk, and explicitly omit them in chatprompt. The SDK may contain storage structs as it already does for internal parts; mark fields internal for generation. No tables or columns are added. One narrowly scoped execution-history trigger migration is required, as described below.

Keep dependencies acyclic:

  1. codersdk owns wire/storage structs and discriminators, with no import of chatd.
  2. chatstructured imports codersdk, the validator, and Fantasy only. It accepts decoded parts or small evidence records, validates schemas/values, builds a Fantasy definition/runner, and computes pure request state. It does not import chatloop, chatprompt, chatstate, db2sdk, or parent chatd.
  3. chatprompt parses database message content and identifies internal-only metadata. db2sdk projects already-committed receipt metadata; it does not run validation or orchestration.
  4. Parent chatd combines parsed rows with their visibility/compression/IDs, invokes pure state logic, constructs chatloop.ProviderTool, and builds transaction messages. Chatstate uses existing parsing helpers for tool bookkeeping, not parent chatd.

Do not create a generic run framework or configurable output-strategy registry.

Every control marker must be visible to the durable-state reader. Attach finish provenance, rejection count evidence, and candidate metadata to existing client-visible assistant/tool rows before their commit. For stop-hook continuation, insert a dedicated visibility=user control row with short continuation text plus internal invalidation metadata in the same transaction as the model-only hook context. The state reader sees this row even after restart; db2sdk strips the metadata, and provider prompts omit the row. Treat both control and receipt rows as non-generation rows in all bookkeeping helpers.

activeStructuredRequest(history) must:

  1. Read original client-visible input rows, including compressed originals, and stop at the latest real user request even if that request has no format. Skip model-only copies and synthetic receipt/control rows.
  2. Bind candidates, failure counts, invalidation markers, and terminal receipts to the persisted request UUID. Duplicate or malformed control metadata fails closed, never by choosing an arbitrary last format.
  3. Treat terminal receipts as closure. Maintenance compaction/clear must not revive a completed request.
  4. Count real assistant generation steps for this request across compaction. Do not count receipts or compaction display rows, or reset repair counts when the runner restarts.
  5. Keep request state out of model prompts except the finalizer definition and a concise system instruction. Do not copy a parent's requirement into a child chat.

5.2 Finalizer inside the ordinary tool loop

Use reserved local tool coder_structured_output. It takes exactly {"output": <value>}. It returns a tiny acknowledgment on validation success or bounded tool-error feedback on failure. Its successful value is recorded in internal candidate metadata in the same step commit as its result, outside truncatable tool text.

Pass a fresh full fantasy.FunctionTool.InputSchema through the existing chatloop.ProviderTool definition plus local Runner seam. This preserves the wrapper root's additionalProperties:false and the caller's schema without changing all ordinary tool definitions. Do not run the caller schema through mutating schema.Normalize. Do not add a new Fantasy interface or enable strict tool decoding globally.

Qualify configured provider strict-tool settings with actual wire encoders. Disable strictness for this finalizer alone where the pinned provider supports a per-tool override. Otherwise fail the incompatible structured request as configuration_error; never alter other tools, require optional properties, or rewrite the caller's semantics to satisfy a strict provider. Add a preflight for any incompatible configuration that can be detected locally.

  1. Keep tool choice automatic, retaining existing reasoning/thinking options. The API's guarantee comes from server validation and completion checks, not forced tool selection. Do not add ToolChoiceRequired everywhere.
  2. Add the finalizer only for the governing request. It is locally handled, never entered into DynamicToolNames, never dispatched to a workspace, and never accepted through client tool-results submission.
  3. Apply existing exclusivity policy to local and dynamic siblings before execution or client dispatch. Two finalizer calls in one batch also fail. Provider-executed tools may already have run upstream; reject that batch's finalizer candidate and require a separate finalization step, without claiming rollback of upstream actions.
  4. Do not reserve the name globally for ordinary chats. For a structured request, reject collisions at declared-dynamic validation and recheck the resolved MCP/builtin/provider/alias registries before every call. Return a configuration failure, not a silently renamed or missing tool.
  5. Preserve tool-call/result pairs and existing tool authorization/hook checks. Candidate state comes only from the server executor, never from model-supplied metadata or a client result.
  6. A refused, interrupted, content-filtered, or token-truncated attempt cannot produce a candidate, even if a partial argument string happens to parse. Preserve finish provenance in the step's internal control metadata so replay cannot execute an apparently complete finalizer from an ineligible attempt.

Pre-persistence argument screening: screen the original finalizer argument bytes before converting or committing any assistant row to JSONB, not only when the tool later runs. JSONB otherwise erases duplicate-key evidence and can reject large numbers or Unicode before validation. Enforce the argument byte cap while receiving its deltas. At stream completion, check raw JSON/Unicode/resource limits and finish eligibility before hook admission or step commit. On rejection, persist a small valid placeholder plus server-only rejected-call metadata, then close the tool call with a bounded error result. Never retain the unsafe payload in JSONB or reinterpret the placeholder as valid arguments on replay. The executor still performs schema validation and must check the persisted eligibility marker. Test duplicate keys, excessive exponents, NUL/surrogate escapes, restart after the assistant commit, and interrupted buffered parts.

5.3 Completion, repair, and hooks

A structured request must bypass ordinary text-only completion and unrelated stop-after success. Do not simply add the finalizer to StopAfterTools and search for any past successful result.

  1. Resolve pending ordinary local/dynamic tools through existing paths. Dynamic pauses keep the request pending. Existing tool timeouts and transport retries remain intact.
  2. Validate a completed finalizer submission against the persisted schema. On failure, commit its error result and failure marker. On valid submission, commit candidate metadata and a short acknowledgment.
  3. Allow the initial unsuccessful finalization plus two repair opportunities: fail on the third rejected finalization batch. Count one rejection per assistant batch with a text-only terminal completion, invalid finalizer arguments, or invalid finalizer batching. Commit concise feedback and continue after the first two. Ordinary intermediate tools and transport retries do not increment this count; hook continuation does not reset it. Persist rejection evidence so compaction/restarts cannot reset or double-count it.
  4. The independent existing maximum-step budget still applies across the request. Reaching either limit without a current candidate is a failure: validation_exhausted if validation failed, otherwise not_produced. Do not let normal max-step handling report success.
  5. Run normal stop hooks for a current candidate. A continuing hook commits an invalidation marker alongside its model context. The model must finalize again; a prior candidate cannot close the resumed turn. The invalidation must survive process restart even though the existing stop-nudge tracker is in memory.
  6. After a hook or other await, reacquire the chat transaction and recheck ownership, runner, history version, attempt where applicable, request UUID, and candidate validity before committing anything. Use existing fences, not a second lock protocol.
  7. Infrastructure/provider errors use existing retry classification. Terminal provider/configuration/refusal failures produce a failed receipt with sanitized text; do not expose raw provider bodies, schema values, or credentials.

5.4 Atomic terminal receipt and cancellation

At successful termination, append the user-visible/model-invisible receipt in the same ChatMachine.Update transaction that invokes FinishTurn, before promotion. For terminal failure, do the equivalent with FinishError. For interruption, include it after partial/cancellation messages but before FinishInterruption promotes the queue.

Use existing CommitStep where legal; extend transition inputs with optional terminal messages only where the current transition cannot append them safely. Reuse message insert/revision publication, not a separate result store. Receipt publication happens only after commit. Failed fence checks produce no receipt. Under the chat lock, an existing receipt for this UUID prevents a duplicate terminal write after ambiguous commit acknowledgment or worker recovery.

Receipt rows have visibility=user, no provider metadata or usage, and normal text plus internal outcome metadata. Explicitly exclude them from completion scans, step counting, unresolved-tool scans, summaries, and provider replay. Visibility filtering alone is not sufficient because generation also reads visible history.

Edit ordering is fixed: under the chat lock, collect affected pending UUIDs and existing terminal outcomes first; delete the old suffix and clear the queue; then insert cancellation receipts; then insert the replacement input and its hook suffix. Include every inserted receipt in EditChatMessageResponse.Messages, in ID order. Do not cancel already-completed requests or delete newly written cancellations in the same edit.

Cancellation of a queued request can append a receipt while an unrelated turn is generating or awaiting a tool. Filter receipt/control rows in chatstate pending-tool helpers as well as generation helpers. Regression gate: deleting a queued structured request must neither hide the ordinary turn's pending local/dynamic calls, rerun completed effects, nor make that turn complete.

Execution-history trigger prerequisite: the current message-insert trigger, introduced in migration 000519_chatd_core_state_machine.up.sql, advances history_version and resets generation_attempt for every inserted message. A receipt-only insert during unrelated work would therefore invalidate its execution fence. Add a migration replacing the current function to exempt only rows with the dedicated terminal-outcome discriminator, role=assistant, and visibility=user. Filter transition-table rows before deriving affected chat IDs; a mixed receipt/ordinary-message batch must retain normal fencing. Do not exempt hook continuation or other user-only/control messages.

Keep message revision, snapshot version, and stream notifications intact. Receipt-only inserts must not reset execution history, attempts, or retry budgets or restart an unrelated task. Recognize the dedicated server-created terminal row shape, not an arbitrary nested occurrence of its discriminator. Public inputs, dynamic-tool results, and hook payloads cannot manufacture this shape. Receipts are immutable. Keep UPDATE/DELETE triggers unchanged: suffix edits must still invalidate execution. The current dump and later trigger replacements confirm the insert-only seam; use the then-current definitions when implementing the migration.

Independent stream watermark prerequisite: stream_loop.go currently gates fetching and emitting message deltas on execution history, although message revision follows snapshot_version. First separate those responsibilities. Fetch on a newer snapshot, query changed messages using the last successfully applied snapshot watermark, and emit message events independently of historyChanged. Preserve existing ID/revision deduplication, after_id reconnect, and tombstone-driven history resets. Keep preview resets and part-sequence clearing tied to execution-history/generation changes, not receipt-only snapshots. Reuse the existing snapshot cursor; no new counter or isolation protocol is needed. ChatMachine.ReadLock takes FOR SHARE on the chat row within a transaction, excluding the FOR UPDATE transitions until its message reads finish. applyDBSnapshot already advances snapshotVersion after assembling message events. Preserve those boundaries: a hint or failed read must never advance the cursor. Test initialization, full reload, failed reads, empty deltas, and concurrent commits. streamLoop.part continues to filter by execution history, attempt, and sequence, so a receipt does not discard buffered parts.

Land stream separation before the trigger migration, and both before any receipt-producing integration. The paired PostgreSQL regression must block an ordinary provider call or dynamic wait, delete a queued structured request, and receive its cancellation through SSE and REST before unblocking. Verify unchanged active task, preview, attempt, and retry accounting; no repeated effect or lost message after resumption; and correct reconnect and edit/reset behavior. Also cover receipt-only and mixed batches, non-exempt controls, malformed discriminator shapes, and rollback atomicity.

Lifecycle operation Required behavior
Queue/promotion Preserve UUID and format. Promotion alone does not create a new request. Receipt for the old turn precedes the new input.
Delete queued request Write cancelled/queue_deleted in the same transaction as deletion. A disconnected client can recover it.
Edit or edit-cleared queue Write cancellation for still-pending discarded requests; then mint a new request UUID for the replacement if formatted. Completed old output follows existing suffix deletion/reset semantics, not a fabricated second outcome.
Interrupt, including busy interrupt Old pending UUID receives cancelled/interrupted; new queued UUID remains separate. No output from partial buffer data.
Worker takeover/transport retry Preserve UUID and retry accounting; repeat only uncommitted work. No public success from an in-memory candidate.
Dynamic pause/timeout Remain pending while paused. Timeout tool errors follow existing continuation rules; the finalizer remains local.
Compaction Recover schema, repair budget, and invalidation state from durable originals. Never resurrect an earlier format after a plain-text request.
Mode switch Reject an incompatible change while affected structured work is pending, including queued work; recheck at execution.
Archive/unarchive Tx.SetArchived is disallowed in active states but permits E1/XE1 with queued work. Preserve queued structured requests and UUIDs as suspended, matching ordinary queue semantics; resume only through existing promotion paths. No cancellation on archive.
Clear Tx.ClearContext accepts settled W/E0 only, with an empty queue. Existing receipts remain closed; clearing context must not restart their schemas.
Reconcile Tx.ReconcileInvalidState synthesizes pending tool cancellations and parks in E0/E1. In the same transaction, close any determinable outstanding active structured request with a failed generation_failed receipt; retain queued requests. Never fabricate success or select an ambiguous request.
Retry/fork No new public retry or fork endpoint is introduced. Internal transport/worker retries keep the request UUID; callers requesting a new execution send or edit input.
Chat deletion/retention Existing resource deletion is terminal; results are no longer available. Document this exception.

6. Reviewable delivery sequence

Start with 14 slices across three phases: foundations (A1-A5), durable runtime (B1-B6), and public API/release qualification (C1-C3). Implement one layer at a time on its actual prerequisite. Do not build the whole feature on one branch and split afterward. Each layer includes its own tests, builds independently, leaves ordinary chats usable, and is safe if later layers are delayed.

Size gate for every row: at most 500 handwritten changed lines, including tests and docs; at most 1000 total only when the excess is named generated output. Targets below are budgets, not measured diff sizes. Re-measure against each layer's immediate base. If a layer cannot fit, split at its stated fallback seam before coding further. Never move required tests into a trailing testing PR.

Phase A: internal foundation, HTTP acceptance still absent

PR One concern and files Test-first gate and stopping point Target handwritten / generated
A1 Bounded, hermetic Draft-07 compiler. New coderd/x/chatd/chatstructured/schema.go and tests; go.mod directness only. Red: malformed schema, unsupported dialect/ref, numeric precision, loader traps, duplicate keys, size/depth/exponent limits. Green: compiler and preflight only. Refactor: remove duplicate parsing. No API field or tool installed. 350-450 / 0-30
A2 Finalizer argument validation and schema-preserving tool definition. chatstructured tool/value helpers and tests. Red: arrays/scalars/null, constraints, fractional bounds, invalid/oversized/duplicate/trailing args, immutable schema, short ack. Green: isolated definition/runner. Refactor: one validation path. Nothing registered at runtime. 300-420 / 0
A3 Internal request/control/outcome codecs and active-request identity. codersdk/chats.go, variant-tag tests, chatstructured/state.go, chatprompt, db2sdk. Red: compressed original, model-only copy, plain-text successor, stripping, duplicate control data. Green: invisible JSONB metadata and state reconstruction. Refactor: centralize parsers, not all message code. No public success or request acceptance. 350-450 / 0-80
A4 Separate message-delta streaming from execution-history changes. stream_loop.go and stream tests. Red: snapshot-only message delta, unchanged preview/attempt, initial load, reconnect, failed read, empty delta, concurrent commit, edit/reset. Green: reuse the safely applied snapshot cursor for message revisions; retain execution-based preview invalidation. Refactor: no redundant cursor. No trigger exception or receipt producer yet. 250-400 / 0
A5 Exempt genuine terminal receipt INSERTs from execution fencing. New migration up/down pair, generated database/dump.sql, PostgreSQL trigger/runner tests. Red: receipt-only versus mixed batch, wrong role/visibility/shape, controls still fence, unchanged UPDATE behavior, preserved snapshot/revision notifications and active attempt. Green: narrow trigger exception with A4 streaming verified. Refactor: one explicit row predicate. No production receipt writer yet. 250-420 / 30-100

Phase A gate: all new package, variant, prompt, db2sdk, PostgreSQL migration/trigger, runner, and stream tests pass. No unknown public part variant; ordinary chat fixtures unchanged. Stream snapshot isolation and preview preservation are proven before the trigger exception lands. If A1 exceeds budget, split resource scanner/loader qualification from schema compilation, keeping tests with both. A1 uses this split as four layers: A1a raw JSON safety boundary, A1b private schema-position vocabulary screening and sanitization, A1c schema resource limits and regexp screening, and A1d trusted meta-validation, deny-all compilation, output validation, and loader qualification. If A3 exceeds budget, land internal codec/stripping before state reconstruction. Phase A ends at A5; A4 and A5 precede every receipt-producing slice.

Phase B: durable outcomes and runtime, still not publicly accepted

PR One concern and files Test-first gate and stopping point Target handwritten / generated
B1 Fenced success/failure receipt commits. generation.go, message_conversion.go, narrow chatstate transition inputs if needed. Red: success/failed receipt atomicity, receipt before promotion, rollback/fence loss, ambiguous commit retry, receipt exclusion from ordinary model/control flow. Green: dormant terminal helper exercised with internal fixtures. Refactor: share only common receipt construction. 350-450 / 0
B2 Close interrupted or reconciled active requests. tasks.go, chatstate/transitions.go and state tests. Red: interrupt before/after candidate, buffered partial output, failed fence, receipt before promotion; reconcile fails the active request but retains queued requests. Green: atomic active-request cancellation/failure. Refactor: reuse terminal construction. No public formatted input yet. 300-420 / 0
B3 Cancel discarded queued/edited requests without disturbing other work. chatd.go, chatstate/transitions.go, stream and state tests. Red: queue deletion during blocked ordinary local/dynamic work; immediate receipt without preview/attempt loss; edit collection/deletion/receipt/replacement order; fresh UUID; archive preserves queue; clear cannot revive completed work. Green: transaction-local cancellation plus the decisive A4/A5 end-to-end regression. Refactor: avoid duplicate queue scans. 350-450 / 0
B4 Screen finalizer bytes before JSONB persistence. generation.go, stream accumulation/step-commit helpers and tests. Red: duplicate keys, huge exponent, NUL/invalid Unicode, over-limit deltas, ineligible finish, interrupted buffer, restart after rejected assistant commit. Green: bounded placeholder and persisted eligibility/rejection evidence; reuse A1/A2 scanner. Refactor: no second JSON validator. Finalizer is not registered yet. 300-430 / 0
B5 Candidate dispatch and provider compatibility. generation_preparer.go, generation.go, chatloop, tool-wire tests. Red: auto choice with thinking, strict-setting compatibility, ordinary tools retained, collision, exclusive local/dynamic/provider batches, eligibility check, candidate not public/truncated. Green: local finalizer for internal requests only, using B4 admission. Refactor: use existing ProviderTool seam. 350-450 / 0
B6 Structured completion and durable corrective continuation. generation.go, chatstructured/state.go, hook tests. Red: text-only repair, invalid then valid, fail on third rejection, compaction/restart budget, hook invalidation and post-await fences. Green: candidate required for success, terminal errors wired. Refactor: keep decision logic pure where practical. Complete internally seeded end-to-end request. 350-450 / 0

Phase B gate: internally seeded formatted requests pass real-PostgreSQL lifecycle tests, live stream-isolation tests, and provider wire tests. None of these layers adds an accepted public request option. Earlier candidate/receipt helpers remain dormant unless an internal test explicitly seeds metadata. Ordinary HTTP chats are unchanged. Phase B ends at B6.

If necessary, split B3 into queued deletion and edited-suffix cancellation, B5 into local tool admission and provider-wire compatibility, or B6 into durable repair decisions and stop-hook finalization. These are fallback boundaries, not permission to exceed limits. Every split retains tests for its own invariants and remains unexposed until complete.

Phase C: public API, documentation, and release qualification

PR One concern and files Test-first gate and stopping point Target handwritten / generated
C1 Project durable receipts and request IDs into Go/API messages. codersdk/chats.go, db2sdk, stream_loop tests and generated artifacts. Red: exact JSON union/null shape, REST equals committed stream/reconnect/reset, internal parts hidden, request ID through queue promotion, legacy FE typecheck. Green: additive output fields. Refactor: shared conversion used by every read path. No HTTP request acceptance yet. 250-380 / 150-450
C2 Activate formatted create/send behind the admission experiment. codersdk/chats.go, codersdk/deployment.go, exp_chats.go, chatd request options and HTTP tests. Red: gate disabled including dev builds; disabled gate does not strand persisted work; ingress validation precedes side effects; owner/ACL behavior; no format unchanged; queue/interrupt; mode/collision failure; server-only metadata. Green: opt-in acceptance of the complete runtime. Refactor: one ingress validation helper. Publish usage, limits, rollout rules, guarantee, and reconnect recipe with the API change. 350-450 / 150-450
C3 Format-aware edits. EditChatMessageRequest, patchChatMessage, chatd edit plumbing and tests. Red: omitted preserves, explicit text clears, replacement format validates before suffix deletion, replacement UUID and supersession receipts. Green: edit options. Refactor: reuse create/send validation. Regenerate changed input artifacts and update edit documentation. 250-400 / 100-300

Critical C2 intermediate behavior: before C3 lands, reject edits of formatted inputs with a documented conflict rather than silently dropping their metadata. Ordinary-message edits keep existing behavior, including cancellation of any structured queued requests they discard. C3 removes that narrow restriction. The public release phase comprises C1-C3; C3 is its final member.

If C2 exceeds its budget, split create activation from send activation. Keep each request type, gate tests, and usage documentation with the endpoint that enforces it. If generation would exceed 1000 total lines, split the affected generator input into separate request/result additions. A regeneration-only follow-up is acceptable only if no intermediate branch has stale required generated files. Do not publish an accepted-but-unenforced request field to shrink a diff.

Delivery mechanics

  1. In Exec mode, rebase planning facts onto the chosen implementation base and install the repository hooks. Use a stable final branch name per layer and gh-stack dependency links, each based on its actual prerequisite. No stack or PR is created by this planning task.
  2. Invoke implement-flow per bounded slice, not once with this entire roadmap. Supply that slice's full contract, dependencies, tests, and remote UAT focus. Each run includes implementation, validation, remote UAT, authorized publication, CI, and exact-head review. Do not skip stages merely because a foundation has no visible feature; remotely run its contract fixtures and ordinary-chat smoke checks, clearly labeled as such.
  3. Freeze scope when a layer is committed or published. Work from the lowest unfinished layer upward. Lower fixes invalidate dependent tests/review evidence; reconcile descendants before claiming readiness.
  4. Deslop, simplify, and clean comments before requesting review. Keep one concern per PR and concise descriptions with generated-file names and exact validation results. Apply current repository review limits and stop on known blockers or failed gates.
  5. Complete each phase's checks and integrated behavior before considering it ready. Publication and merge follow the authorization and repository gates in effect then; this plan is not merge authorization.

7. Validation and acceptance

7.1 Automated checks

Each behavior starts with a failing test, then minimal implementation, then a simplification pass with tests still passing. Use existing fixtures in chatd_test.go, generation_preparer_internal_test.go, message_conversion_test.go, hooks_test.go/stop_test.go, tasks_test.go, chatstate/transitions*_test.go, exp_chats*_test.go, and chatloop/chattest wire helpers. Add focused files rather than expanding giant test functions. Concurrent tests use t.Parallel(), unique IDs, channels or quartz, never timing sleeps.

Representative targeted commands after the named packages/tests exist:

umask 022
go test ./coderd/x/chatd/chatstructured -count=1
go test ./codersdk -run '^TestChatMessagePartVariantTags$' -count=1
go test ./coderd/database/db2sdk ./coderd/x/chatd/chatprompt -count=1
go test ./coderd/x/chatd/chatloop ./coderd/x/chatd/chatstate -count=1
go test ./coderd/x/chatd -run 'StructuredOutput|Stop|Compaction|Dynamic' -count=1
go test ./coderd -run 'StructuredOutput' -count=1

Verify new test names actually match; a command reporting no tests is not a gate. Run full touched-package tests after targeted passes. Run lifecycle tests with real PostgreSQL and race detection using repository fixtures, plus go test -race for new shared-state logic. Include worker takeover, duplicate completion, rollback, and interrupt-vs-finalize races with deterministic barriers.

Provider qualification covers actual pinned request encoders with mock HTTP servers: OpenAI Responses, OpenAI-compatible Chat Completions, and Anthropic with thinking. Prove the schema wrapper and tools remain present, finalizer stays local, malformed/partial output cannot succeed, and transcript replay works. Other transports are not claimed validated without evidence. Provider rejection remains an explicit failure, not text fallback.

Generate the actual affected outputs with make site/src/api/typesGenerated.ts and make coderd/apidoc/swagger.json, then run make gen as required by the final diff. make gen covers more than API artifacts and can require Docker/PostgreSQL. Inspect every generated delta. Run make fmt, make lint, make pre-commit, and make pre-push; do not bypass hooks or report an unavailable service as a pass. Generated TS still requires the frontend type/lint gates and frontend-review; no handwritten frontend logic is planned.

Follow write-docs if authoring under docs/, and run its formatting/lint checks. Add only TODO notes in affected chatd/ARCHITECTURE.md sections; the human author writes architecture prose.

7.2 Acceptance checklist

  1. With the experiment enabled, create/send accepts a bounded schema and edit obeys explicit preserve/replace/clear semantics. Disabled acceptance returns a field error even on dev builds but does not strand persisted work. No format preserves ordinary behavior.
  2. Schema and value validation is hermetic, precision-preserving, bounded, and rejects unsupported assertions rather than dropping them.
  3. Normal tools, dynamic pause/resume, provider reasoning, and subagent use remain functional. The finalizer is server-owned and never client-executed.
  4. Only a current validated candidate can finish. Text-only, malformed, duplicate, mixed, refused, and incomplete attempts cannot be reported as success.
  5. Repair and step budgets survive compaction and restart. Hook continuation invalidates stale candidates durably.
  6. A fenced terminal transaction writes one current outcome per request UUID before queue promotion. Failed ownership or rollback emits no success.
  7. Queue removal, interruption, and supersession do not leave accepted requests waiting forever. A queued cancellation streams immediately while unrelated work is blocked, without changing its attempt, retries, preview, or pending effects. Edits/reset/deletion retain documented semantics.
  8. REST history and committed message replay expose identical values and errors. JSON null succeeds distinctly from absent output. Legacy clients see a usable text fallback without a new content union variant.
  9. Each delivered PR stays within size limits, passes its own tests/CI, and includes generated outputs. Final combined head passes the cross-layer scenarios below.
  10. Remote UAT evidence includes screenshots and playable video, not only logs. The final implementation decision records the exact tested SHA and any real blockers outside this plan.

8. Remote dogfooding and evidence

Use coder-agents-uat through an independent runner on an exact pushed feature SHA. Do not replace remote UAT with a local server in this workspace. No deployment actions are part of the current planning task.

Setup and safety gate

  1. Verify the pushed feature ref and exact SHA. Create or reuse a Coder Agents chat through MCP; record its URL, actual workspace/template, SHA, round start, absolute deadline, and evidence directory. Use at most three implementation UAT rounds before reporting a blocker.
  2. Inside the remote workspace, start an isolated test deployment using the project's dogfood skill and ./scripts/develop.sh. Use a unique process/resource/port namespace. For a focused binary build, use go build -o <owned-scratch>/coder ./cmd/coder, not full make build during iteration. On C2 and later, explicitly enable chat-structured-output only on the test deployment after verifying its migration and worker versions; also exercise the disabled-admission case. Earlier slices use internal contract fixtures, not a development bypass.
  3. All test CLI calls use the repo's ./scripts/coder-dev.sh or an equivalent verified shim with a dedicated config and explicit test URL. HTTP uses UAT_URL and a token minted on that test deployment. Never use shared-deployment tokens or bare coder for mutations.
  4. The remote agent stops after attaching an isolation proof. The runner verifies test URL, identity, different shared host, and leak-audit evidence before sending GO. An empty inherited URL is not proof; verify the hosting deployment from the chat/workspace identity. Repeat the gate after restarts and for later rounds.
  5. Bind cleanup to owned processes, browser sessions, files, and resources. Never sweep other sessions or mutate the hosting deployment. Audit shared-deployment activity after each settle; unavailable required evidence is a blocker, not an assumed pass.

Hands-on scenarios

Use a small Go/curl harness outside tracked source, not the future TypeScript adapter. Save sanitized requests, committed stream events, REST reads, and validation assertions.

  1. Basic values: request object, array, nullable union, and null; validate results independently and match UUIDs.
  2. Real tools: ask the agent to inspect a harmless fixture file and return its content and path. Verify workspace tool use precedes finalization; no client finalizer definition exists.
  3. Dynamic pause: require a dynamic tool before finalization, fulfill only that action, disconnect/reconnect, and retrieve the same terminal receipt through REST.
  4. Queue and cancellation: queue a second schema, interrupt the first, delete a queued request, and edit a formatted message. Match each success/cancellation to the correct UUID. With an ordinary turn held in a dynamic wait, delete a queued structured request and observe its receipt through SSE/REST before releasing the wait; prove the active preview and tool call survive. Then send an ordinary message and prove the earlier schema is not reused.
  5. Hooks and recovery: with a deterministic test provider/hook fixture, emit candidate A, continue from a stop hook, compact or restart the worker, then emit B. Prove A never became public output, budgets survived, and exactly one B receipt committed.
  6. Negative cases: invalid schema, remote/file refs, duplicate keys, huge exponent, oversized output, invalid-then-valid value, and exhausted repairs. Validation errors must not interrupt an existing valid turn before acceptance.
  7. Provider and compatibility smoke: real configured OpenAI/Anthropic tool-capable models where credentials exist, including thinking; inspect an ordinary chat and a receipt in the real Coder UI with agent-browser. Use snapshots for functionality. Label fake-provider lifecycle tests separately from live-model evidence.

Capture PNG screenshots and a playable MP4/WebM covering the steps and final assertions. Logs, script, or an asciinema cast supplement the evidence but do not replace video. Use a uniquely named agent-browser session; record the real UI or a terminal recording with rendered frames. Attach screenshots for inspection and inspect semantic video frames, not merely file existence or decoder success. If media capture fails, report that gate unverified.

Keep evidence out of git, under the run's .mux-uat/round-N/ convention or the current skill's equivalent. Preserve the chat URL, manifest, exact tested SHA, commands, provider modes, raw failure receipts, screenshots, and recording. When attaching evidence to authorized GitHub work, use gh ... --attach with matching local Markdown paths and verify published links. Do not use browser cookies or gh-image.

9. Accepted trade-offs and stopping rules

  1. Inline Draft-07 deliberately excludes references and newer dialects. The later AI SDK adapter must emit a compatible schema or reject locally; adding safe reference expansion is separate work.
  2. One internal finalizer plus durable message receipts adds more lifecycle handling than a raw custom tool, but removes that protocol from every API consumer. No run table, public strategy selector, or new transport is introduced.
  3. JSON output is atomic rather than partially streamed. Tool transcript and normal text can still stream, but are not validated output.
  4. Request UUID metadata avoids new tables and columns and survives queue promotion without relying on ephemeral TurnID. A narrow trigger migration separates terminal receipt publication from execution-history fencing. The default-off admission experiment has no development bypass. Enable it only after all serving/worker replicas and the migration support this contract; disabling acceptance must not stop recovery of already-accepted work. Do not claim a rolling mixed-version or downgrade guarantee before draining accepted work.
  5. If a required protection cannot fit a PR, split before continuing. If scope grows into provider rewrites, a new persistence subsystem, or frontend behavior changes, stop and revise the relevant slice instead of quietly expanding it.
  6. Do not mark implementation complete until all acceptance gates pass on the final current head. Missing provider access, Docker, remote UAT, video, or CI is an explicit blocker. Planning has verified code paths, not run these future implementation checks.

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

Chat messages gain two optional fields. structured_output_request_id is
set on a user message that asked for a structured output, and on a
queued message that does; promotion keeps it. structured_output is set
on the receipt that closed a request and carries its outcome, with the
value passed through as raw JSON, so a succeeded null stays an explicit
null. Content is unchanged: internal parts stay hidden and a receipt
keeps its text fallback. Malformed or ambiguous metadata leaves the
fields unset and never drops the message. Ordinary messages serialize
exactly as before.

The shared db2sdk conversion parses each message once and fills the
fields, so REST, the stream snapshot, message events, history resets,
queue updates and send and edit responses stay identical.

Generated by make gen: coderd/apidoc/docs.go, coderd/apidoc/swagger.json,
docs/reference/api/chats.md, docs/reference/api/schemas.md and
site/src/api/typesGenerated.ts.

_Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high_

Signed-off-by: Thomas Kosiewski <[email protected]>
A structured output value can be any JSON value, so the generated TypeScript type must be unknown rather than the global Record<string, string> mapping for raw JSON.

_Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high_

Signed-off-by: Thomas Kosiewski <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@ThomasK33

ThomasK33 commented Sep 26, 2026 •

Copy link
Copy Markdown
Member Author

Remote UAT on 54e77c7ee9812f867bb0ade35f53aae1256d1342: PASS (round 1), with no issues and no known limitation reproduced. No API opens a structured request yet, so the end-to-end check writes requests and receipts through the chatstate transitions and drives them with a scripted fake provider on the test deployment.

  • Runner: a Coder Agents chat on dogfood (model Opus 5.5 on Bedrock) with an isolated ./scripts/develop.sh test deployment on 127.0.0.1:3100. Binaries report vcs.revision=54e77c7e and vcs.modified=false.
  • Native gates on a private PostgreSQL 13 bound to 127.0.0.1 (every setup step's exit code recorded): every chatstate test, the whole chatd package, the coderd chat message, queue, edit and stream tests (including TestChatMessagesProjectStructuredOutput), the db2sdk and codersdk tests (including TestChatMessage_StructuredOutput), the migration tests and the earlier layers' tests all pass. -race, three fuzz targets, the benchmarks, go vet and go build exited 0. The dump generator and go mod tidy left no diff. A strace run of the test binary made one connection, to the loopback test server.
  • Generated files: a check reran apidocgen, the docs formatters and apitypings, compared all 31 generated docs files and the other generated files with the committed ones (no difference), typechecked the frontend and confirmed readonly value?: unknown; on ChatStructuredOutput.
  • Probe, plain and with -race: every required projection case passes (request ID parsing, receipt shapes, malformed and ambiguous metadata, raw JSON pass-through including a succeeded null).
  • End-to-end projection check on the deployment: for a request, succeeded and failed receipts, a queued request ID with its queue_update, a queue deletion receipt and a promoted request ID, the raw REST JSON, the live stream and a fresh snapshot agree; ordinary messages have no new keys; a live model chat matched across REST and the stream; the fake provider and model were deleted at the end.
  • Chat smoke in the real UI, recorded in two segments: login, UAT model, prompt, live PONG reply in the chat pane, then the chat after a reload. The collector reviewed both recordings frame by frame against a clock overlay. Cleanup was verified.

Logged in during the recording
Prompt typed
Live model reply
Chat after reload

Recording, segment 1 (login, prompt, live reply):

98c7a89d-c-ui-recording.webm

Recording, segment 2 (reloaded chat):

d8f5cd84-c-ui-recording-seg2.webm

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

@ThomasK33
ThomasK33 added this pull request to stack #29881 September 26, 2026 15:42
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T15:47:05.220428Z 54e77c7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54e77c7ee9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/apidoc/swagger.json

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant