Files
coder/coderd/x/chatd/chatstate/state.go
T
Michael Suchacz 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.
2026-07-13 20:28:28 +02:00

167 lines
5.5 KiB
Go

package chatstate
import (
"github.com/coder/coder/v2/coderd/database"
)
// ExecutionState identifies a chat's current execution state. Values
// outside the chat execution state model are represented by
// [StateInvalid].
type ExecutionState string
const (
// StateN: chat does not exist.
StateN ExecutionState = "N"
// StateW: waiting, empty queue, not archived.
StateW ExecutionState = "W"
// StateE0: error, empty queue, not archived.
StateE0 ExecutionState = "E0"
// StateE1: error, non-empty queue, not archived.
StateE1 ExecutionState = "E1"
// StateR0: running, empty queue, not archived.
StateR0 ExecutionState = "R0"
// StateR1: running, non-empty queue, not archived.
StateR1 ExecutionState = "R1"
// StateI0: interrupting, empty queue, not archived.
StateI0 ExecutionState = "I0"
// StateI1: interrupting, non-empty queue, not archived.
StateI1 ExecutionState = "I1"
// StateA0: requires_action, empty queue, not archived.
StateA0 ExecutionState = "A0"
// StateA1: requires_action, non-empty queue, not archived.
StateA1 ExecutionState = "A1"
// StateXW: archived waiting, empty queue.
StateXW ExecutionState = "XW"
// StateXE0: archived error, empty queue.
StateXE0 ExecutionState = "XE0"
// StateXE1: archived error, non-empty queue.
StateXE1 ExecutionState = "XE1"
// StateInvalid groups every status/archive/queue combination that
// is not one of the valid states above. The state machine refuses
// non-reconciliation transitions on invalid states and exposes the
// [Tx.ReconcileInvalidState] transition to recover.
StateInvalid ExecutionState = "Invalid"
)
// String implements fmt.Stringer.
func (s ExecutionState) String() string { return string(s) }
// AllExecutionStates is the canonical enumeration of every value the
// classifier can return. Tests rely on this list to iterate over every
// state when verifying transition coverage.
var AllExecutionStates = []ExecutionState{
StateN,
StateW,
StateE0,
StateE1,
StateR0,
StateR1,
StateI0,
StateI1,
StateA0,
StateA1,
StateXW,
StateXE0,
StateXE1,
StateInvalid,
}
// IsRunnable returns true for the execution states that the chat
// worker is allowed to acquire and drive forward: R0, R1, I0, I1,
// A0, and A1. Requires-action states need worker ownership for
// timeout processing. Other states are idle (W, E*, XW, XE*), absent
// (N), or invalid.
func (s ExecutionState) IsRunnable() bool {
switch s {
case StateR0, StateR1, StateI0, StateI1, StateA0, StateA1:
return true
default:
return false
}
}
// IsArchived returns true for the three archived execution states.
func (s ExecutionState) IsArchived() bool {
switch s {
case StateXW, StateXE0, StateXE1:
return true
default:
return false
}
}
// QueueNonEmpty returns true for execution states that require a
// non-empty queue. Useful when seeding test fixtures.
func (s ExecutionState) QueueNonEmpty() bool {
switch s {
case StateE1, StateR1, StateI1, StateA1, StateXE1:
return true
default:
return false
}
}
// ClassifyExecutionState turns the chat row, queue cardinality, and
// whether the chat row exists into an [ExecutionState]. The caller is
// responsible for loading the chat under the row lock and reading the
// queue count in the same transaction.
//
// Callers that have no chat row (lookup returned sql.ErrNoRows)
// should pass exists=false; the chat, status, and archive arguments
// are then ignored.
//
// The classifier is a single flat switch over the valid (status,
// archived, queue) tuples in the chat execution state model. Anything
// outside that set (archived busy states, waiting with a non-empty
// queue, future enum values) falls through to [StateInvalid].
//
//nolint:revive // queueNonEmpty/exists are simple classifier inputs.
func ClassifyExecutionState(chat database.Chat, queueNonEmpty, exists bool) ExecutionState {
if !exists {
return StateN
}
switch {
case chat.Status == database.ChatStatusWaiting && !chat.Archived && !queueNonEmpty:
return StateW
case chat.Status == database.ChatStatusWaiting && chat.Archived && !queueNonEmpty:
return StateXW
case chat.Status == database.ChatStatusError && !chat.Archived && !queueNonEmpty:
return StateE0
case chat.Status == database.ChatStatusError && !chat.Archived && queueNonEmpty:
return StateE1
case chat.Status == database.ChatStatusError && chat.Archived && !queueNonEmpty:
return StateXE0
case chat.Status == database.ChatStatusError && chat.Archived && queueNonEmpty:
return StateXE1
case chat.Status == database.ChatStatusRunning && !chat.Archived && !queueNonEmpty:
return StateR0
case chat.Status == database.ChatStatusRunning && !chat.Archived && queueNonEmpty:
return StateR1
case chat.Status == database.ChatStatusInterrupting && !chat.Archived && !queueNonEmpty:
return StateI0
case chat.Status == database.ChatStatusInterrupting && !chat.Archived && queueNonEmpty:
return StateI1
case chat.Status == database.ChatStatusRequiresAction && !chat.Archived && !queueNonEmpty:
return StateA0
case chat.Status == database.ChatStatusRequiresAction && !chat.Archived && queueNonEmpty:
return StateA1
}
return StateInvalid
}
// OwnershipState identifies whether a chat row is currently owned by a
// worker. The state machine treats execution and ownership as
// orthogonal.
type OwnershipState string
const (
// StateU: chat has no owner (worker_id IS NULL).
StateU OwnershipState = "U"
// StateO: chat has an owner (worker_id IS NOT NULL).
StateO OwnershipState = "O"
)
// AllOwnershipStates is the canonical enumeration of ownership states.
var AllOwnershipStates = []OwnershipState{StateU, StateO}