fix(agents): persist system prompt server-side instead of localStorage (#22857)

## Problem

The Admin → Agents → System Prompt textarea saved only to the browser's
`localStorage`. The value was never sent to the backend, never stored in
the database, and never injected into chats. Entering text, clicking
Save, and refreshing the page showed no changes — the prompt was
effectively a no-op.

## Root Cause

Three disconnected layers:
1. **Frontend** wrote to `localStorage`, never called an API.
2. **`handleCreateChat`** never read `savedSystemPrompt`.
3. **Backend** hardcoded `chatd.DefaultSystemPrompt` on every chat
creation — no field in `CreateChatRequest` accepted a custom prompt.

## Changes

### Database
- Added `GetChatSystemPrompt` / `UpsertChatSystemPrompt` queries on the
existing `site_configs` table (no migration needed).

### API
- `GET /api/experimental/chats/system-prompt` — returns the configured
prompt (any authenticated user).
- `PUT /api/experimental/chats/system-prompt` — sets the prompt
(admin-only, `rbac: deployment_config update`).
- Input validation: max 32 KiB prompt length.

### Backend
- `resolvedChatSystemPrompt(ctx)` checks for a custom prompt in the DB,
falls back to `chatd.DefaultSystemPrompt` when empty/unset.
- Logs a warning on DB errors instead of silently swallowing them.
- Replaced the hardcoded `defaultChatSystemPrompt()` call in chat
creation.

### Frontend
- Replaced `localStorage` read/write with React Query
`useQuery`/`useMutation` backed by the new endpoints.
- Fixed `useEffect` draft sync to avoid clobbering in-progress user
edits on refetch.
- Added `try/catch` error handling on save (draft stays dirty for
retry).
- Save button disabled during mutation (`isSavingSystemPrompt`).
- Query key follows kebab-case convention (`chat-system-prompt`).

### UX
- Added hint: "When empty, the built-in default prompt is used."

### Tests
- `TestChatSystemPrompt`: GET returns empty when unset, admin can set,
non-admin gets 403.
- dbauthz `TestMethodTestSuite` coverage for both new querier methods.
This commit is contained in:
Cian Johnston
2026-03-10 11:46:53 +00:00
committed by GitHub
parent a21f00d250
commit c933ddcffd
18 changed files with 365 additions and 32 deletions
+22
View File
@@ -14475,6 +14475,18 @@ func (q *sqlQuerier) GetApplicationName(ctx context.Context) (string, error) {
return value, err
}
const getChatSystemPrompt = `-- name: GetChatSystemPrompt :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt
`
func (q *sqlQuerier) GetChatSystemPrompt(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatSystemPrompt)
var chat_system_prompt string
err := row.Scan(&chat_system_prompt)
return chat_system_prompt, err
}
const getCoordinatorResumeTokenSigningKey = `-- name: GetCoordinatorResumeTokenSigningKey :one
SELECT value FROM site_configs WHERE key = 'coordinator_resume_token_signing_key'
`
@@ -14689,6 +14701,16 @@ func (q *sqlQuerier) UpsertApplicationName(ctx context.Context, value string) er
return err
}
const upsertChatSystemPrompt = `-- name: UpsertChatSystemPrompt :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_system_prompt', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_system_prompt'
`
func (q *sqlQuerier) UpsertChatSystemPrompt(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatSystemPrompt, value)
return err
}
const upsertCoordinatorResumeTokenSigningKey = `-- name: UpsertCoordinatorResumeTokenSigningKey :exec
INSERT INTO site_configs (key, value) VALUES ('coordinator_resume_token_signing_key', $1)
ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'coordinator_resume_token_signing_key'