// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 // source: chat.sql package db import ( "context" "github.com/jackc/pgx/v5/pgtype" ) const chatSessionHasUserMessage = `-- name: ChatSessionHasUserMessage :one SELECT EXISTS ( SELECT 1 FROM chat_message WHERE chat_session_id = $1 AND role = 'user' ) AS has_user_message ` // Reports whether a session has any human (role='user') message yet. Used to // scope the is_agent_intro self-introduction prompt to the very first, // server-driven turn: an intro session starts with zero user messages, so the // opening run gets the "introduce yourself" prompt. Once the creator replies, // later turns in the same session must fall back to the normal reply prompt // instead of repeating the introduction every turn (MUL-4259). func (q *Queries) ChatSessionHasUserMessage(ctx context.Context, chatSessionID pgtype.UUID) (bool, error) { row := q.db.QueryRow(ctx, chatSessionHasUserMessage, chatSessionID) var has_user_message bool err := row.Scan(&has_user_message) return has_user_message, err } const createChatMessage = `-- name: CreateChatMessage :one INSERT INTO chat_message (chat_session_id, role, content, task_id, failure_reason, elapsed_ms) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, chat_session_id, role, content, task_id, created_at, failure_reason, elapsed_ms ` type CreateChatMessageParams struct { ChatSessionID pgtype.UUID `json:"chat_session_id"` Role string `json:"role"` Content string `json:"content"` TaskID pgtype.UUID `json:"task_id"` FailureReason pgtype.Text `json:"failure_reason"` ElapsedMs pgtype.Int8 `json:"elapsed_ms"` } func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) (ChatMessage, error) { row := q.db.QueryRow(ctx, createChatMessage, arg.ChatSessionID, arg.Role, arg.Content, arg.TaskID, arg.FailureReason, arg.ElapsedMs, ) var i ChatMessage err := row.Scan( &i.ID, &i.ChatSessionID, &i.Role, &i.Content, &i.TaskID, &i.CreatedAt, &i.FailureReason, &i.ElapsedMs, ) return i, err } const createChatSession = `-- name: CreateChatSession :one INSERT INTO chat_session (workspace_id, agent_id, creator_id, title, runtime_id, is_agent_intro) VALUES ($1, $2, $3, $4, (SELECT runtime_id FROM agent WHERE id = $2), $5) RETURNING id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at, unread_since, runtime_id, last_read_at, is_agent_intro, pinned_at ` type CreateChatSessionParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` AgentID pgtype.UUID `json:"agent_id"` CreatorID pgtype.UUID `json:"creator_id"` Title string `json:"title"` IsAgentIntro bool `json:"is_agent_intro"` } func (q *Queries) CreateChatSession(ctx context.Context, arg CreateChatSessionParams) (ChatSession, error) { row := q.db.QueryRow(ctx, createChatSession, arg.WorkspaceID, arg.AgentID, arg.CreatorID, arg.Title, arg.IsAgentIntro, ) var i ChatSession err := row.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, ) return i, err } const createChatTask = `-- name: CreateChatTask :one INSERT INTO agent_task_queue ( agent_id, runtime_id, issue_id, status, priority, chat_session_id, initiator_user_id, originator_user_id, force_fresh_session, runtime_mcp_overlay, runtime_connected_apps ) VALUES ( $1, $2, NULL, 'queued', $3, $4, $5, $6, COALESCE($7::boolean, FALSE), $8, $9 ) RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids ` type CreateChatTaskParams struct { AgentID pgtype.UUID `json:"agent_id"` RuntimeID pgtype.UUID `json:"runtime_id"` Priority int32 `json:"priority"` ChatSessionID pgtype.UUID `json:"chat_session_id"` InitiatorUserID pgtype.UUID `json:"initiator_user_id"` OriginatorUserID pgtype.UUID `json:"originator_user_id"` ForceFreshSession pgtype.Bool `json:"force_fresh_session"` RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"` RuntimeConnectedApps []byte `json:"runtime_connected_apps"` } func (q *Queries) CreateChatTask(ctx context.Context, arg CreateChatTaskParams) (AgentTaskQueue, error) { row := q.db.QueryRow(ctx, createChatTask, arg.AgentID, arg.RuntimeID, arg.Priority, arg.ChatSessionID, arg.InitiatorUserID, arg.OriginatorUserID, arg.ForceFreshSession, arg.RuntimeMcpOverlay, arg.RuntimeConnectedApps, ) var i AgentTaskQueue err := row.Scan( &i.ID, &i.AgentID, &i.IssueID, &i.Status, &i.Priority, &i.DispatchedAt, &i.StartedAt, &i.CompletedAt, &i.Result, &i.Error, &i.CreatedAt, &i.Context, &i.RuntimeID, &i.SessionID, &i.WorkDir, &i.TriggerCommentID, &i.ChatSessionID, &i.AutopilotRunID, &i.Attempt, &i.MaxAttempts, &i.ParentTaskID, &i.FailureReason, &i.TriggerSummary, &i.ForceFreshSession, &i.IsLeaderTask, &i.WaitReason, &i.InitiatorUserID, &i.HandoffNote, &i.PrepareLeaseExpiresAt, &i.SquadID, &i.RuntimeMcpOverlay, &i.EscalationForTaskID, &i.FireAt, &i.OriginatorUserID, &i.RuntimeConnectedApps, &i.CoalescedCommentIds, &i.DeliveredCommentIds, ) return i, err } const deleteChatSession = `-- name: DeleteChatSession :exec DELETE FROM chat_session WHERE id = $1 AND workspace_id = $2 ` type DeleteChatSessionParams struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Hard delete. chat_message rows cascade via FK ON DELETE CASCADE; the // chat_session_id on agent_task_queue is set NULL by FK so completed/failed // task history survives the session being removed. Callers MUST run inside // the same transaction that holds LockChatSessionForDelete and that has // already cancelled any in-flight tasks (see CancelAgentTasksByChatSession) // so the daemon does not keep running work whose result has nowhere to // land. workspace_id in the WHERE clause is a SQL-layer tenant guard; see // DeleteIssue. func (q *Queries) DeleteChatSession(ctx context.Context, arg DeleteChatSessionParams) error { _, err := q.db.Exec(ctx, deleteChatSession, arg.ID, arg.WorkspaceID) return err } const deleteUserChatMessageByTask = `-- name: DeleteUserChatMessageByTask :one DELETE FROM chat_message WHERE task_id = $1 AND role = 'user' RETURNING id, chat_session_id, role, content, task_id, created_at, failure_reason, elapsed_ms ` func (q *Queries) DeleteUserChatMessageByTask(ctx context.Context, taskID pgtype.UUID) (ChatMessage, error) { row := q.db.QueryRow(ctx, deleteUserChatMessageByTask, taskID) var i ChatMessage err := row.Scan( &i.ID, &i.ChatSessionID, &i.Role, &i.Content, &i.TaskID, &i.CreatedAt, &i.FailureReason, &i.ElapsedMs, ) return i, err } const getChatMessage = `-- name: GetChatMessage :one SELECT id, chat_session_id, role, content, task_id, created_at, failure_reason, elapsed_ms FROM chat_message WHERE id = $1 ` func (q *Queries) GetChatMessage(ctx context.Context, id pgtype.UUID) (ChatMessage, error) { row := q.db.QueryRow(ctx, getChatMessage, id) var i ChatMessage err := row.Scan( &i.ID, &i.ChatSessionID, &i.Role, &i.Content, &i.TaskID, &i.CreatedAt, &i.FailureReason, &i.ElapsedMs, ) return i, err } const getChatSession = `-- name: GetChatSession :one SELECT id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at, unread_since, runtime_id, last_read_at, is_agent_intro, pinned_at FROM chat_session WHERE id = $1 ` func (q *Queries) GetChatSession(ctx context.Context, id pgtype.UUID) (ChatSession, error) { row := q.db.QueryRow(ctx, getChatSession, id) var i ChatSession err := row.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, ) return i, err } const getChatSessionInWorkspace = `-- name: GetChatSessionInWorkspace :one SELECT id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at, unread_since, runtime_id, last_read_at, is_agent_intro, pinned_at FROM chat_session WHERE id = $1 AND workspace_id = $2 ` type GetChatSessionInWorkspaceParams struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } func (q *Queries) GetChatSessionInWorkspace(ctx context.Context, arg GetChatSessionInWorkspaceParams) (ChatSession, error) { row := q.db.QueryRow(ctx, getChatSessionInWorkspace, arg.ID, arg.WorkspaceID) var i ChatSession err := row.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, ) return i, err } const getLastChatTaskSession = `-- name: GetLastChatTaskSession :one SELECT session_id, work_dir, runtime_id FROM agent_task_queue WHERE chat_session_id = $1 AND ( status = 'completed' OR ( status = 'failed' AND COALESCE(failure_reason, '') NOT IN ('iteration_limit', 'agent_fallback_message', 'api_invalid_request', 'codex_semantic_inactivity') AND NOT (COALESCE(error, '') ILIKE '%400%' AND COALESCE(error, '') ILIKE '%invalid_request_error%') ) ) AND session_id IS NOT NULL ORDER BY completed_at DESC LIMIT 1 ` type GetLastChatTaskSessionRow struct { SessionID pgtype.Text `json:"session_id"` WorkDir pgtype.Text `json:"work_dir"` RuntimeID pgtype.UUID `json:"runtime_id"` } // Returns the most recent task in this chat session that managed to record a // session_id. Includes both completed and failed tasks: even a failed task // may have established a real agent session before failing, and we'd rather // resume there than start over and lose conversation memory. Used as a // fallback when chat_session.session_id is NULL. Resume-unsafe failures are // excluded because replaying those sessions deterministically reproduces the // same terminal state. func (q *Queries) GetLastChatTaskSession(ctx context.Context, chatSessionID pgtype.UUID) (GetLastChatTaskSessionRow, error) { row := q.db.QueryRow(ctx, getLastChatTaskSession, chatSessionID) var i GetLastChatTaskSessionRow err := row.Scan(&i.SessionID, &i.WorkDir, &i.RuntimeID) return i, err } const getMostRecentUserChatMessage = `-- name: GetMostRecentUserChatMessage :one SELECT id, chat_session_id, role, content, task_id, created_at, failure_reason, elapsed_ms FROM chat_message WHERE chat_session_id = $1 AND role = 'user' ORDER BY created_at DESC LIMIT 1 ` // Returns the most recent role='user' message in a session. Used by the // Lark `/issue` command parser: when the user types `/issue` with no // title, the spec falls back to "use the previous user message as the // title". Bot replies (role='assistant') are excluded — only human // input qualifies as a fallback title source. func (q *Queries) GetMostRecentUserChatMessage(ctx context.Context, chatSessionID pgtype.UUID) (ChatMessage, error) { row := q.db.QueryRow(ctx, getMostRecentUserChatMessage, chatSessionID) var i ChatMessage err := row.Scan( &i.ID, &i.ChatSessionID, &i.Role, &i.Content, &i.TaskID, &i.CreatedAt, &i.FailureReason, &i.ElapsedMs, ) return i, err } const getPendingChatTask = `-- name: GetPendingChatTask :one SELECT id, status, created_at FROM agent_task_queue WHERE chat_session_id = $1 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory') ORDER BY created_at DESC LIMIT 1 ` type GetPendingChatTaskRow struct { ID pgtype.UUID `json:"id"` Status string `json:"status"` CreatedAt pgtype.Timestamptz `json:"created_at"` } // Returns the most recent in-flight task for a chat session, if any. // Used by the frontend to recover pending state after refresh / reopen. // created_at is the anchor for the chat StatusPill timer (it computes // elapsed = now - task.created_at), so the pill survives refresh / reopen // without "resetting to 0s". func (q *Queries) GetPendingChatTask(ctx context.Context, chatSessionID pgtype.UUID) (GetPendingChatTaskRow, error) { row := q.db.QueryRow(ctx, getPendingChatTask, chatSessionID) var i GetPendingChatTaskRow err := row.Scan(&i.ID, &i.Status, &i.CreatedAt) return i, err } const hasPendingChatTasksByCreator = `-- name: HasPendingChatTasksByCreator :one SELECT EXISTS ( SELECT 1 FROM agent_task_queue atq JOIN chat_session cs ON cs.id = atq.chat_session_id WHERE atq.chat_session_id IS NOT NULL AND atq.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory') AND cs.workspace_id = $1 AND cs.creator_id = $2 AND cs.agent_id = ANY($3::uuid[]) ) AS has_pending ` type HasPendingChatTasksByCreatorParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` CreatorID pgtype.UUID `json:"creator_id"` AgentIds []pgtype.UUID `json:"agent_ids"` } // Boolean fast-path for the FAB's "running" indicator. Returns a single // EXISTS row instead of the full task list, so the planner can stop at the // first matching in-flight task (LIMIT 1 semantics via EXISTS). // // Permission filtering is baked into the query: agent_id = ANY($3) restricts // the result to the agents the caller may currently see, so a member who lost // access to a private agent never gets a true from a task they can no longer // reach. The handler must pass its resolved accessible-agent id set as $3; // an empty array yields false. func (q *Queries) HasPendingChatTasksByCreator(ctx context.Context, arg HasPendingChatTasksByCreatorParams) (bool, error) { row := q.db.QueryRow(ctx, hasPendingChatTasksByCreator, arg.WorkspaceID, arg.CreatorID, arg.AgentIds) var has_pending bool err := row.Scan(&has_pending) return has_pending, err } const linkChatMessageToTask = `-- name: LinkChatMessageToTask :exec UPDATE chat_message SET task_id = $2 WHERE id = $1 AND role = 'user' ` type LinkChatMessageToTaskParams struct { ID pgtype.UUID `json:"id"` TaskID pgtype.UUID `json:"task_id"` } func (q *Queries) LinkChatMessageToTask(ctx context.Context, arg LinkChatMessageToTaskParams) error { _, err := q.db.Exec(ctx, linkChatMessageToTask, arg.ID, arg.TaskID) return err } const listAllChatSessionsByCreator = `-- name: ListAllChatSessionsByCreator :many SELECT cs.id, cs.workspace_id, cs.agent_id, cs.creator_id, cs.title, cs.session_id, cs.work_dir, cs.status, cs.created_at, cs.updated_at, cs.unread_since, cs.runtime_id, cs.last_read_at, cs.is_agent_intro, cs.pinned_at, (SELECT count(*) FROM chat_message m WHERE m.chat_session_id = cs.id AND m.role = 'assistant' AND m.created_at > cs.last_read_at)::int AS unread_count, COALESCE(lm.content, '') AS last_message_content, COALESCE(lm.role, '') AS last_message_role, lm.created_at AS last_message_at, lm.failure_reason AS last_message_failure_reason FROM chat_session cs LEFT JOIN LATERAL ( SELECT content, role, created_at, failure_reason FROM chat_message m WHERE m.chat_session_id = cs.id ORDER BY m.created_at DESC LIMIT 1 ) lm ON true WHERE cs.workspace_id = $1 AND cs.creator_id = $2 ORDER BY (cs.pinned_at IS NOT NULL) DESC, cs.pinned_at DESC, COALESCE(lm.created_at, cs.updated_at) DESC ` type ListAllChatSessionsByCreatorParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` CreatorID pgtype.UUID `json:"creator_id"` } type ListAllChatSessionsByCreatorRow struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` AgentID pgtype.UUID `json:"agent_id"` CreatorID pgtype.UUID `json:"creator_id"` Title string `json:"title"` SessionID pgtype.Text `json:"session_id"` WorkDir pgtype.Text `json:"work_dir"` Status string `json:"status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` UnreadSince pgtype.Timestamptz `json:"unread_since"` RuntimeID pgtype.UUID `json:"runtime_id"` LastReadAt pgtype.Timestamptz `json:"last_read_at"` IsAgentIntro bool `json:"is_agent_intro"` PinnedAt pgtype.Timestamptz `json:"pinned_at"` UnreadCount int32 `json:"unread_count"` LastMessageContent string `json:"last_message_content"` LastMessageRole string `json:"last_message_role"` LastMessageAt pgtype.Timestamptz `json:"last_message_at"` LastMessageFailureReason pgtype.Text `json:"last_message_failure_reason"` } func (q *Queries) ListAllChatSessionsByCreator(ctx context.Context, arg ListAllChatSessionsByCreatorParams) ([]ListAllChatSessionsByCreatorRow, error) { rows, err := q.db.Query(ctx, listAllChatSessionsByCreator, arg.WorkspaceID, arg.CreatorID) if err != nil { return nil, err } defer rows.Close() items := []ListAllChatSessionsByCreatorRow{} for rows.Next() { var i ListAllChatSessionsByCreatorRow if err := rows.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, &i.UnreadCount, &i.LastMessageContent, &i.LastMessageRole, &i.LastMessageAt, &i.LastMessageFailureReason, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listChatMessages = `-- name: ListChatMessages :many SELECT id, chat_session_id, role, content, task_id, created_at, failure_reason, elapsed_ms FROM chat_message WHERE chat_session_id = $1 ORDER BY created_at ASC ` func (q *Queries) ListChatMessages(ctx context.Context, chatSessionID pgtype.UUID) ([]ChatMessage, error) { rows, err := q.db.Query(ctx, listChatMessages, chatSessionID) if err != nil { return nil, err } defer rows.Close() items := []ChatMessage{} for rows.Next() { var i ChatMessage if err := rows.Scan( &i.ID, &i.ChatSessionID, &i.Role, &i.Content, &i.TaskID, &i.CreatedAt, &i.FailureReason, &i.ElapsedMs, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listChatMessagesPage = `-- name: ListChatMessagesPage :many SELECT id, chat_session_id, role, content, task_id, created_at, failure_reason, elapsed_ms FROM chat_message WHERE chat_session_id = $1 AND ( $3::timestamptz IS NULL OR (created_at, id) < ($3::timestamptz, $4::uuid) ) ORDER BY created_at DESC, id DESC LIMIT $2 ` type ListChatMessagesPageParams struct { ChatSessionID pgtype.UUID `json:"chat_session_id"` Limit int32 `json:"limit"` BeforeCreatedAt pgtype.Timestamptz `json:"before_created_at"` BeforeID pgtype.UUID `json:"before_id"` } func (q *Queries) ListChatMessagesPage(ctx context.Context, arg ListChatMessagesPageParams) ([]ChatMessage, error) { rows, err := q.db.Query(ctx, listChatMessagesPage, arg.ChatSessionID, arg.Limit, arg.BeforeCreatedAt, arg.BeforeID, ) if err != nil { return nil, err } defer rows.Close() items := []ChatMessage{} for rows.Next() { var i ChatMessage if err := rows.Scan( &i.ID, &i.ChatSessionID, &i.Role, &i.Content, &i.TaskID, &i.CreatedAt, &i.FailureReason, &i.ElapsedMs, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listChatSessionsByCreator = `-- name: ListChatSessionsByCreator :many SELECT cs.id, cs.workspace_id, cs.agent_id, cs.creator_id, cs.title, cs.session_id, cs.work_dir, cs.status, cs.created_at, cs.updated_at, cs.unread_since, cs.runtime_id, cs.last_read_at, cs.is_agent_intro, cs.pinned_at, (SELECT count(*) FROM chat_message m WHERE m.chat_session_id = cs.id AND m.role = 'assistant' AND m.created_at > cs.last_read_at)::int AS unread_count, COALESCE(lm.content, '') AS last_message_content, COALESCE(lm.role, '') AS last_message_role, lm.created_at AS last_message_at, lm.failure_reason AS last_message_failure_reason FROM chat_session cs LEFT JOIN LATERAL ( SELECT content, role, created_at, failure_reason FROM chat_message m WHERE m.chat_session_id = cs.id ORDER BY m.created_at DESC LIMIT 1 ) lm ON true WHERE cs.workspace_id = $1 AND cs.creator_id = $2 AND cs.status = 'active' ORDER BY (cs.pinned_at IS NOT NULL) DESC, cs.pinned_at DESC, COALESCE(lm.created_at, cs.updated_at) DESC ` type ListChatSessionsByCreatorParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` CreatorID pgtype.UUID `json:"creator_id"` } type ListChatSessionsByCreatorRow struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` AgentID pgtype.UUID `json:"agent_id"` CreatorID pgtype.UUID `json:"creator_id"` Title string `json:"title"` SessionID pgtype.Text `json:"session_id"` WorkDir pgtype.Text `json:"work_dir"` Status string `json:"status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` UnreadSince pgtype.Timestamptz `json:"unread_since"` RuntimeID pgtype.UUID `json:"runtime_id"` LastReadAt pgtype.Timestamptz `json:"last_read_at"` IsAgentIntro bool `json:"is_agent_intro"` PinnedAt pgtype.Timestamptz `json:"pinned_at"` UnreadCount int32 `json:"unread_count"` LastMessageContent string `json:"last_message_content"` LastMessageRole string `json:"last_message_role"` LastMessageAt pgtype.Timestamptz `json:"last_message_at"` LastMessageFailureReason pgtype.Text `json:"last_message_failure_reason"` } // IM-style list: each active session with its unread *count* (assistant // messages after the read cursor), a preview of the latest message, and // ordered by most-recent activity so a new reply bumps a session to the top. func (q *Queries) ListChatSessionsByCreator(ctx context.Context, arg ListChatSessionsByCreatorParams) ([]ListChatSessionsByCreatorRow, error) { rows, err := q.db.Query(ctx, listChatSessionsByCreator, arg.WorkspaceID, arg.CreatorID) if err != nil { return nil, err } defer rows.Close() items := []ListChatSessionsByCreatorRow{} for rows.Next() { var i ListChatSessionsByCreatorRow if err := rows.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, &i.UnreadCount, &i.LastMessageContent, &i.LastMessageRole, &i.LastMessageAt, &i.LastMessageFailureReason, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listPendingChatTasksByCreator = `-- name: ListPendingChatTasksByCreator :many SELECT atq.id AS task_id, atq.status, atq.chat_session_id, cs.agent_id FROM agent_task_queue atq JOIN chat_session cs ON cs.id = atq.chat_session_id WHERE atq.chat_session_id IS NOT NULL AND atq.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory') AND cs.workspace_id = $1 AND cs.creator_id = $2 ORDER BY atq.created_at DESC ` type ListPendingChatTasksByCreatorParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` CreatorID pgtype.UUID `json:"creator_id"` } type ListPendingChatTasksByCreatorRow struct { TaskID pgtype.UUID `json:"task_id"` Status string `json:"status"` ChatSessionID pgtype.UUID `json:"chat_session_id"` AgentID pgtype.UUID `json:"agent_id"` } // Aggregate view of all in-flight chat tasks owned by a given creator in a // workspace. Drives the FAB's "running" indicator when the chat window is // closed and no single session's query is active. // // Returns cs.agent_id so the handler can filter tasks belonging to private // agents the caller has lost access to using the already-loaded `allowed` // set — no second ListAllChatSessionsByCreator scan on the hot path. // // atq.chat_session_id IS NOT NULL is redundant given the JOIN, but stated // explicitly so the planner can prove the query predicate is a subset of the // idx_agent_task_queue_chat_pending_v2 partial-index predicate and use it. func (q *Queries) ListPendingChatTasksByCreator(ctx context.Context, arg ListPendingChatTasksByCreatorParams) ([]ListPendingChatTasksByCreatorRow, error) { rows, err := q.db.Query(ctx, listPendingChatTasksByCreator, arg.WorkspaceID, arg.CreatorID) if err != nil { return nil, err } defer rows.Close() items := []ListPendingChatTasksByCreatorRow{} for rows.Next() { var i ListPendingChatTasksByCreatorRow if err := rows.Scan( &i.TaskID, &i.Status, &i.ChatSessionID, &i.AgentID, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const lockChatSessionForDelete = `-- name: LockChatSessionForDelete :one SELECT id FROM chat_session WHERE id = $1 FOR UPDATE ` // Acquires an exclusive (FOR UPDATE) row lock on chat_session(id). Used by // the delete path so that a concurrent SendChatMessage cannot enqueue a new // agent_task_queue row referencing this session between our cancel and // delete steps. The FK from agent_task_queue.chat_session_id takes a // KEY SHARE lock on the parent row during INSERT validation, which // conflicts with FOR UPDATE — concurrent inserts block here and then fail // their FK check after we commit the delete. func (q *Queries) LockChatSessionForDelete(ctx context.Context, id pgtype.UUID) (pgtype.UUID, error) { row := q.db.QueryRow(ctx, lockChatSessionForDelete, id) err := row.Scan(&id) return id, err } const markChatSessionRead = `-- name: MarkChatSessionRead :exec UPDATE chat_session SET last_read_at = now() WHERE id = $1 ` // Advances the read cursor to now, dropping the session's unread_count to 0. func (q *Queries) MarkChatSessionRead(ctx context.Context, id pgtype.UUID) error { _, err := q.db.Exec(ctx, markChatSessionRead, id) return err } const setChatSessionArchived = `-- name: SetChatSessionArchived :one UPDATE chat_session SET status = CASE WHEN $2::bool THEN 'archived' ELSE 'active' END, updated_at = now() WHERE id = $1 RETURNING id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at, unread_since, runtime_id, last_read_at, is_agent_intro, pinned_at ` type SetChatSessionArchivedParams struct { ID pgtype.UUID `json:"id"` Archived bool `json:"archived"` } // Archive/unarchive a chat session by flipping status between 'active' and // 'archived'. Bumps updated_at so the row re-sorts on the receiving list. The // send-message path refuses archived sessions (see SendChatMessage), so the // conversation is effectively read-only until it is unarchived. func (q *Queries) SetChatSessionArchived(ctx context.Context, arg SetChatSessionArchivedParams) (ChatSession, error) { row := q.db.QueryRow(ctx, setChatSessionArchived, arg.ID, arg.Archived) var i ChatSession err := row.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, ) return i, err } const setChatSessionPinned = `-- name: SetChatSessionPinned :one UPDATE chat_session SET pinned_at = CASE WHEN $2::bool THEN COALESCE(pinned_at, now()) ELSE NULL END WHERE id = $1 RETURNING id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at, unread_since, runtime_id, last_read_at, is_agent_intro, pinned_at ` type SetChatSessionPinnedParams struct { ID pgtype.UUID `json:"id"` Pinned bool `json:"pinned"` } // Pin/unpin a chat. Deliberately does NOT touch updated_at: pinning is a // list-ordering preference, not activity, so it must not bump the session's // last-activity sort key (which would make an unpinned chat jump the list). // pinned = true stamps pinned_at only when it was NULL, so re-pinning keeps // the original pin order; pinned = false clears it. func (q *Queries) SetChatSessionPinned(ctx context.Context, arg SetChatSessionPinnedParams) (ChatSession, error) { row := q.db.QueryRow(ctx, setChatSessionPinned, arg.ID, arg.Pinned) var i ChatSession err := row.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, ) return i, err } const touchChatSession = `-- name: TouchChatSession :exec UPDATE chat_session SET updated_at = now() WHERE id = $1 ` func (q *Queries) TouchChatSession(ctx context.Context, id pgtype.UUID) error { _, err := q.db.Exec(ctx, touchChatSession, id) return err } const updateChatSessionSession = `-- name: UpdateChatSessionSession :exec UPDATE chat_session SET session_id = COALESCE($1, session_id), work_dir = COALESCE($2, work_dir), runtime_id = COALESCE($3, runtime_id), updated_at = now() WHERE id = $4 ` type UpdateChatSessionSessionParams struct { SessionID pgtype.Text `json:"session_id"` WorkDir pgtype.Text `json:"work_dir"` RuntimeID pgtype.UUID `json:"runtime_id"` ID pgtype.UUID `json:"id"` } // Updates the resume pointer for a chat session. Empty/NULL inputs are // ignored via COALESCE so a task that completes without a session_id (e.g. // the agent crashed before establishing one) cannot wipe out a previously // recorded resume pointer. This makes the chat memory robust against // intermittent agent failures. func (q *Queries) UpdateChatSessionSession(ctx context.Context, arg UpdateChatSessionSessionParams) error { _, err := q.db.Exec(ctx, updateChatSessionSession, arg.SessionID, arg.WorkDir, arg.RuntimeID, arg.ID, ) return err } const updateChatSessionTitle = `-- name: UpdateChatSessionTitle :one UPDATE chat_session SET title = $2, updated_at = now() WHERE id = $1 RETURNING id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at, unread_since, runtime_id, last_read_at, is_agent_intro, pinned_at ` type UpdateChatSessionTitleParams struct { ID pgtype.UUID `json:"id"` Title string `json:"title"` } func (q *Queries) UpdateChatSessionTitle(ctx context.Context, arg UpdateChatSessionTitleParams) (ChatSession, error) { row := q.db.QueryRow(ctx, updateChatSessionTitle, arg.ID, arg.Title) var i ChatSession err := row.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, ) return i, err } const updateChatSessionTitleIfCurrent = `-- name: UpdateChatSessionTitleIfCurrent :one UPDATE chat_session SET title = $1, updated_at = now() WHERE id = $2 AND title = $3 RETURNING id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at, unread_since, runtime_id, last_read_at, is_agent_intro, pinned_at ` type UpdateChatSessionTitleIfCurrentParams struct { NewTitle string `json:"new_title"` ID pgtype.UUID `json:"id"` ExpectedTitle string `json:"expected_title"` } // Compare-and-swap the title: only overwrite it when it still equals the // value the caller observed (@expected_title). This is the idempotency / // no-clobber guard behind LLM auto-titling (MUL-4295): the async generator // captures the session's current (default/original) title before calling the // model, and this write lands only if a manual rename or a competing writer // has not changed the title in the meantime. A mismatch returns pgx.ErrNoRows // (zero rows updated), which the caller treats as "someone renamed it — leave // it alone", NOT as an error. func (q *Queries) UpdateChatSessionTitleIfCurrent(ctx context.Context, arg UpdateChatSessionTitleIfCurrentParams) (ChatSession, error) { row := q.db.QueryRow(ctx, updateChatSessionTitleIfCurrent, arg.NewTitle, arg.ID, arg.ExpectedTitle) var i ChatSession err := row.Scan( &i.ID, &i.WorkspaceID, &i.AgentID, &i.CreatorID, &i.Title, &i.SessionID, &i.WorkDir, &i.Status, &i.CreatedAt, &i.UpdatedAt, &i.UnreadSince, &i.RuntimeID, &i.LastReadAt, &i.IsAgentIntro, &i.PinnedAt, ) return i, err }