mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: allow manual chat compaction from the error state (#28022)
A chat that fails generation with a context overflow (for example `Input
length 262625 exceeds the maximum allowed input length of 262112
tokens`) is stuck in a catch-22: `POST /chats/{id}/compact` returns 409
because the `RequestCompaction` transition is only allowed from the
waiting state, and the only other way out of the error state is sending
or editing a message, which re-runs generation with the same oversized
prompt and fails again. Compaction is exactly the recovery a
context-overflowed chat needs, and it is unreachable exactly when it is
needed.
Three semantic changes:
- Allow `RequestCompaction` from the error states: `E0 -> R0` and `E1 ->
R1` (queued messages are preserved and processed after the compaction
turn).
- Clear `last_error` in `Tx.RequestCompaction`, matching the
architecture rule that transitions leaving `E0`/`E1` clear the stored
error. Without this a successful compaction would land in waiting with a
stale persisted error.
- Grant the compaction turn a fresh history epoch: a
`grant_history_epoch` flag on `UpdateChatExecutionState` sets
`history_version = snapshot_version`, resets `generation_attempt`, and
clears `retry_state` in the same atomic update that clears `last_error`
(mirroring the `chat_messages` trigger postcondition). The transition
inserts no history, so without this the turn inherits the failed turn's
spent retry budget, and resetting the counter alone could collide with
message part episode keys still retained on the erroring replica.
No frontend change is required: the chat input is already enabled in the
error state and `/compact` submission already handles both the success
and 409 paths. Also updates ARCHITECTURE.md (transition matrix,
endpoint, and manual compaction sections), the endpoint's swagger
description, and SDK comments.
> Mux created this PR on Mike's behalf.
<!-- mux-attribution: model=claude-sonnet-4-6 thinking=high -->
This commit is contained in:
Generated
+10
-1
@@ -11420,9 +11420,12 @@ WITH updated_chat AS (
|
||||
last_error = $5::jsonb,
|
||||
requires_action_deadline_at = $6::timestamptz,
|
||||
compaction_requested_at = $7::timestamptz,
|
||||
history_version = CASE WHEN $8::boolean THEN snapshot_version ELSE history_version END,
|
||||
generation_attempt = CASE WHEN $8::boolean THEN 0 ELSE generation_attempt END,
|
||||
retry_state = CASE WHEN $8::boolean THEN NULL ELSE retry_state END,
|
||||
pin_order = CASE WHEN $2::boolean THEN 0 ELSE pin_order END,
|
||||
updated_at = NOW()
|
||||
WHERE id = $8::uuid
|
||||
WHERE id = $9::uuid
|
||||
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
|
||||
),
|
||||
chats_expanded AS (
|
||||
@@ -11490,6 +11493,7 @@ type UpdateChatExecutionStateParams struct {
|
||||
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
|
||||
RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"`
|
||||
CompactionRequestedAt sql.NullTime `db:"compaction_requested_at" json:"compaction_requested_at"`
|
||||
GrantHistoryEpoch bool `db:"grant_history_epoch" json:"grant_history_epoch"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
@@ -11498,6 +11502,10 @@ type UpdateChatExecutionStateParams struct {
|
||||
// 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.
|
||||
func (q *sqlQuerier) UpdateChatExecutionState(ctx context.Context, arg UpdateChatExecutionStateParams) (Chat, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateChatExecutionState,
|
||||
arg.Status,
|
||||
@@ -11507,6 +11515,7 @@ func (q *sqlQuerier) UpdateChatExecutionState(ctx context.Context, arg UpdateCha
|
||||
arg.LastError,
|
||||
arg.RequiresActionDeadlineAt,
|
||||
arg.CompactionRequestedAt,
|
||||
arg.GrantHistoryEpoch,
|
||||
arg.ID,
|
||||
)
|
||||
var i Chat
|
||||
|
||||
Reference in New Issue
Block a user