Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions coderd/database/queries/chats.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2479,6 +2479,10 @@ FROM chats_expanded;
-- requires-action deadline, and the manual compaction request marker.
-- Callers compose this with transition mutations inside a single
-- ChatMachine.Update transaction.
--
-- grant_history_epoch gives a turn that inserts no history the same
-- fresh retry budget and message part episode keys a history change
-- would grant, mirroring the chat_messages trigger postcondition.
WITH updated_chat AS (
UPDATE chats
SET
Expand All @@ -2489,6 +2493,9 @@ WITH updated_chat AS (
last_error = sqlc.narg('last_error')::jsonb,
requires_action_deadline_at = sqlc.narg('requires_action_deadline_at')::timestamptz,
compaction_requested_at = sqlc.narg('compaction_requested_at')::timestamptz,
history_version = CASE WHEN @grant_history_epoch::boolean THEN snapshot_version ELSE history_version END,
generation_attempt = CASE WHEN @grant_history_epoch::boolean THEN 0 ELSE generation_attempt END,
retry_state = CASE WHEN @grant_history_epoch::boolean THEN NULL ELSE retry_state END,
pin_order = CASE WHEN @archived::boolean THEN 0 ELSE pin_order END,
updated_at = NOW()
WHERE id = @id::uuid
Expand Down
12 changes: 5 additions & 7 deletions coderd/exp_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -3455,9 +3455,10 @@ func (api *API) interruptChat(rw http.ResponseWriter, r *http.Request) {
// @Router /api/experimental/chats/{chat}/compact [post]
// @x-apidocgen {"skip": true}
// @Description Experimental: this endpoint is subject to change.
// @Description Requests a manual context compaction on an idle chat. The
// @Description compaction runs asynchronously through the chat worker and
// @Description bypasses the automatic usage threshold.
// @Description Requests a manual context compaction on an idle or errored
// @Description chat, clearing any stored error. The compaction runs
// @Description asynchronously through the chat worker and bypasses the
// @Description automatic usage threshold.
func (api *API) compactChat(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
Expand Down Expand Up @@ -3498,12 +3499,9 @@ func (api *API) compactChat(rw http.ResponseWriter, r *http.Request) {
Detail: "The chat has no conversation to summarize after the latest compaction.",
})
case errors.Is(err, chatstate.ErrTransitionNotAllowed):
// Covers every non-waiting state: running, interrupting,
// requires-action, and error. "Busy" would misdescribe an
// errored chat, so keep the message state-neutral.
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: "Cannot compact the chat in its current state.",
Detail: "Compaction is only available while the chat is idle.",
Detail: "Compaction is not available while the chat is generating.",
})
default:
logger.Error(ctx, "failed to compact chat", slog.Error(err))
Expand Down
30 changes: 30 additions & 0 deletions coderd/exp_chats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9289,6 +9289,35 @@ func TestCompactChat(t *testing.T) {
require.False(t, persisted.CompactionRequestedAt.Valid)
})

t.Run("FromErrorState", func(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitLong)
client, db := newChatClientWithDatabase(t)
user := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModelConfig(t, client)
chat := seedCompactableChat(t, db, user.OrganizationID, user.UserID, modelConfig.ID)

_, err := db.UpdateChatStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateChatStatusParams{
ID: chat.ID,
Status: database.ChatStatusError,
LastError: pqtype.NullRawMessage{
RawMessage: json.RawMessage(`{"message":"context overflow"}`),
Valid: true,
},
})
require.NoError(t, err)

// Response snapshot only: a worker may already be mutating
// the persisted row.
compacted, err := client.CompactChat(ctx, chat.ID)
require.NoError(t, err)
require.Equal(t, chat.ID, compacted.ID)
require.Equal(t, codersdk.ChatStatusRunning, compacted.Status)
require.Nil(t, compacted.LastError,
"compaction from the error state clears last_error")
})

t.Run("Busy", func(t *testing.T) {
t.Parallel()

Expand All @@ -9310,6 +9339,7 @@ func TestCompactChat(t *testing.T) {
_, err = client.CompactChat(ctx, chat.ID)
sdkErr := requireSDKError(t, err, http.StatusConflict)
require.Contains(t, sdkErr.Message, "Cannot compact the chat in its current state")
require.Contains(t, sdkErr.Detail, "Compaction is not available while the chat is generating.")
})

t.Run("Archived", func(t *testing.T) {
Expand Down
12 changes: 8 additions & 4 deletions coderd/x/chatd/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ I don't recommend reading the rest of section thoroughly if this is your first t
- `PromoteQueuedMessage(qid)` makes a queued message the next message to process. It reorders the queue, interrupts active work, cancels pending dynamic-tool action, or promotes into history immediately as required by the input state.
- `Interrupt(reason)` requests cancellation of an active generation or closes pending dynamic-tool action. It preserves queued backlog.
- `CompleteRequiresAction(results)` inserts submitted tool-result messages followed by any caller-provided suffix messages, clears `requires_action_deadline_at`, and lands in `running`. It preserves queued messages.
- `RequestCompaction` records a manual compaction request on an idle chat by setting `compaction_requested_at` and landing in `running` without inserting any message. The chat worker picks the chat up like any other running chat and consumes the request. See [Manual compaction](#manual-compaction).
- `RequestCompaction` records a manual compaction request on an idle or errored chat by setting `compaction_requested_at` and landing in `running` without inserting any message. It clears `last_error` per the leave-error rule, advances `history_version` to the transaction's new `snapshot_version`, and resets `generation_attempt`, so the compaction turn gets a full retry budget and message part episode keys that cannot collide with episodes retained from the previous turn. The chat worker picks the chat up like any other running chat and consumes the request. See [Manual compaction](#manual-compaction).

### Transitions used by the chat worker

Expand Down Expand Up @@ -154,10 +154,12 @@ stateDiagram-v2

E0 --> R0: SendMessage
E0 --> R0: EditMessage
E0 --> R0: RequestCompaction
E0 --> XE0: SetArchived(true)

E1 --> R1: SendMessage
E1 --> R0: EditMessage
E1 --> R1: RequestCompaction
E1 --> E0: DeleteQueuedMessage / removed last queued
E1 --> E1: DeleteQueuedMessage / queue still non-empty
E1 --> R0: PromoteQueuedMessage / promoted last queued
Expand Down Expand Up @@ -549,8 +551,10 @@ No other input states are supported.
This endpoint uses `RequestCompaction`:

- `W -> RequestCompaction -> R0`
- `E0 -> RequestCompaction -> R0`
- `E1 -> RequestCompaction -> R1`

No other input states are supported: busy chats get a conflict error, and archived chats are rejected. The endpoint is owner-only because the compaction runs LLM inference with the owner's delegated credentials. Inside the same transaction, after the transition succeeds, the endpoint verifies there is at least one uncompressed assistant message after the latest compaction boundary and rolls back with a "nothing to compact" conflict otherwise, so no LLM call is ever started for an empty or already-compacted chat. See [Manual compaction](#manual-compaction) for how the worker consumes the request.
No other input states are supported: generating chats get a conflict error, and archived chats are rejected. Requesting compaction from an error state clears `last_error`, so a context-overflowed chat can recover by compacting instead of re-running the same oversized prompt. The endpoint is owner-only because the compaction runs LLM inference with the owner's delegated credentials. Inside the same transaction, after the transition succeeds, the endpoint verifies there is at least one uncompressed assistant message after the latest compaction boundary and rolls back with a "nothing to compact" conflict otherwise, so no LLM call is ever started for an empty or already-compacted chat. See [Manual compaction](#manual-compaction) for how the worker consumes the request.

## Pubsub

Expand Down Expand Up @@ -951,10 +955,10 @@ Compaction reduces the LLM prompt size by summarizing older history into a compr

Users can also request a compaction on demand via `POST /api/experimental/chats/{chat}/compact` (surfaced in the web UI as the `/compact` slash command). Manual compaction is a durable one-shot request executed through the normal worker loop rather than synchronously in the HTTP handler. This reuses the worker's lock fencing, retry accounting, streamed "Summarizing..." progress parts, metrics, and debug runs, and it survives replica crashes. The flow:

1. The endpoint applies the `RequestCompaction` transition: only allowed from `W`, sets `chats.compaction_requested_at = now()`, lands in `R0` without inserting any message, and publishes a status-change pubsub event to wake workers. A timestamp is used instead of a boolean for debuggability. AI Gateway attribution needs no per-request key: generation preparation resolves the owner's synthetic API key like any other turn.
1. The endpoint applies the `RequestCompaction` transition: allowed from `W`, `E0`, and `E1`, it sets `chats.compaction_requested_at = now()`, clears `last_error`, lands in `R0` (or `R1` from `E1`, preserving the queue) without inserting any message, and publishes a status-change pubsub event to wake workers. Because the transition inserts no history, it advances `history_version` to the transaction's new `snapshot_version` and resets `generation_attempt` itself, granting the fresh retry budget and episode keys a history change would otherwise provide. A timestamp is used instead of a boolean for debuggability. AI Gateway attribution needs no per-request key: generation preparation resolves the owner's synthetic API key like any other turn.
2. The generation goroutine's decision logic checks `compaction_requested_at` after the unresolved local/dynamic tool guards but before the history-completeness check (an idle chat's history is otherwise complete, which would end the turn). If the marker is set and at least one uncompressed assistant message exists after the latest compaction boundary, it selects a forced compaction; if there is nothing to compact, the marker is ignored and the turn finishes normally, clearing it.
3. A forced compaction bypasses the automatic threshold gates (usage below threshold, unknown context window, and the threshold=100 disable) and stamps `source: "manual"` instead of `source: "automatic"` into the `chat_summarized` tool call arguments, tool result JSON, and streamed parts so clients can render manual compactions distinctly.
4. The compaction `CommitStep` consumes the request by clearing `compaction_requested_at` in the same transaction that commits the summary triplet. The next decision pass finds the history complete and finishes the turn, so the chat returns to `waiting` with no assistant follow-up. A `post_compact` hook effect is the one exception: because the decision reads user-visible history, an effect that commits a user-visible message leaves the history incomplete and the turn continues with an assistant response. A model-only effect such as `model_context` reaches the model without resuming generation.
4. The compaction `CommitStep` consumes the request by clearing `compaction_requested_at` in the same transaction that commits the summary triplet. The next decision pass finds the history complete and finishes the turn, so a chat with an empty queue returns to `waiting` with no assistant follow-up; a chat compacted from `E1` proceeds to its queued messages instead. A `post_compact` hook effect is the one exception: because the decision reads user-visible history, an effect that commits a user-visible message leaves the history incomplete and the turn continues with an assistant response. A model-only effect such as `model_context` reaches the model without resuming generation.

The `compaction_requested_at` marker is one-shot: transitions that keep an active turn alive (`Acquire`, `Abandon`, `SetArchived`, queueing a message on a busy chat) carry it forward, while every other transition that rewrites the execution state (`FinishTurn`, `FinishError`, `Interrupt`, `EditMessage`, `PromoteQueuedMessage`, `CancelRequiresAction`, `ReconcileInvalidState`, and so on) clears it by construction, so a stale request can never replay on a later turn.

Expand Down
14 changes: 7 additions & 7 deletions coderd/x/chatd/chatd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2279,16 +2279,16 @@ func (p *Server) InterruptChat(

// CompactChat records a manual compaction request through the
// chatstate.RequestCompaction transition and wakes workers. The chat
// must be idle (waiting); the worker then generates and commits the
// compaction summary through the normal generation loop, bypassing
// the usage threshold, and the chat returns to waiting with no
// assistant follow-up unless a post_compact hook commits a
// user-visible message, which leaves the history incomplete and
// resumes generation.
// must be idle (waiting) or errored; the request clears any stored
// error. The worker then generates and commits the compaction summary
// through the normal generation loop, bypassing the usage threshold,
// and the chat returns to waiting with no assistant follow-up unless
// queued messages remain or a post_compact hook commits a
// user-visible message.
//
// Returns the post-transition chat and an error so callers can map
// state conflicts deliberately: archived chats return ErrChatArchived,
// non-idle chats return a chatstate.ErrTransitionNotAllowed wrapper,
// generating chats return a chatstate.ErrTransitionNotAllowed wrapper,
// and chats with no compactable conversation return
// ErrNothingToCompact.
func (p *Server) CompactChat(
Expand Down
Loading
Loading