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.
This commit is contained in:
Michael Suchacz
2026-07-29 13:16:29 +02:00
committed by GitHub
parent 5d2a69d85a
commit 91c7232d97
11 changed files with 331 additions and 70 deletions
+13 -9
View File
@@ -10270,6 +10270,7 @@ func (q *sqlQuerier) InsertAgentContextResourcesIntoChat(ctx context.Context, ar
const insertChat = `-- name: InsertChat :one
WITH inserted_chat AS (
INSERT INTO chats (
id,
organization_id,
owner_id,
workspace_id,
@@ -10287,7 +10288,7 @@ INSERT INTO chats (
dynamic_tools,
client_type
) VALUES (
$1::uuid,
COALESCE($1::uuid, gen_random_uuid()),
$2::uuid,
$3::uuid,
$4::uuid,
@@ -10295,14 +10296,15 @@ INSERT INTO chats (
$6::uuid,
$7::uuid,
$8::uuid,
$9::text,
$10::chat_mode,
$11::chat_plan_mode,
$12::chat_status,
COALESCE($13::uuid[], '{}'::uuid[]),
COALESCE($14::jsonb, '{}'::jsonb),
$15::jsonb,
$16::chat_client_type
$9::uuid,
$10::text,
$11::chat_mode,
$12::chat_plan_mode,
$13::chat_status,
COALESCE($14::uuid[], '{}'::uuid[]),
COALESCE($15::jsonb, '{}'::jsonb),
$16::jsonb,
$17::chat_client_type
)
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
),
@@ -10365,6 +10367,7 @@ FROM chats_expanded
`
type InsertChatParams struct {
ID uuid.NullUUID `db:"id" json:"id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
@@ -10385,6 +10388,7 @@ type InsertChatParams struct {
func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, insertChat,
arg.ID,
arg.OrganizationID,
arg.OwnerID,
arg.WorkspaceID,