Files
LWLT-AIBOT/control-plane/migrations/005_agent_task_sessions.sql
2026-08-25 10:33:29 +08:00

95 lines
4.0 KiB
SQL

-- A business task owns exactly one external Agent session. The external
-- session identifier and every conversational turn stay server-side and are
-- encrypted by the application before persistence.
CREATE TABLE IF NOT EXISTS agent_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
task_id uuid NOT NULL UNIQUE REFERENCES tasks(id) ON DELETE CASCADE,
conversation_id text,
provider text NOT NULL DEFAULT 'superagent',
external_session_id_ciphertext text,
status text NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'active', 'recovering', 'expired', 'failed', 'closed')),
current_turn integer NOT NULL DEFAULT 0 CHECK (current_turn >= 0),
recovery_count integer NOT NULL DEFAULT 0 CHECK (recovery_count >= 0),
last_error text,
last_message_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS agent_sessions_conversation_idx
ON agent_sessions (organization_id, conversation_id, updated_at DESC)
WHERE conversation_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS agent_sessions_pending_idx
ON agent_sessions (organization_id, conversation_id, status, updated_at DESC)
WHERE conversation_id IS NOT NULL AND status NOT IN ('closed', 'failed');
CREATE TABLE IF NOT EXISTS agent_session_messages (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
agent_session_id uuid NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
task_id uuid NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
turn_no integer NOT NULL CHECK (turn_no > 0),
role text NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
content_ciphertext text NOT NULL,
status text NOT NULL DEFAULT 'queued'
CHECK (status IN ('queued', 'sending', 'sent', 'replayed', 'failed')),
idempotency_key text,
error_code text,
created_at timestamptz NOT NULL DEFAULT now(),
sent_at timestamptz,
UNIQUE (agent_session_id, turn_no, role)
);
CREATE UNIQUE INDEX IF NOT EXISTS agent_session_messages_idempotency_idx
ON agent_session_messages (organization_id, idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS agent_session_messages_queue_idx
ON agent_session_messages (agent_session_id, status, role, turn_no);
CREATE INDEX IF NOT EXISTS agent_session_messages_task_idx
ON agent_session_messages (organization_id, task_id, turn_no);
-- Existing tasks were created before task-scoped sessions existed. Backfill a
-- pending session and preserve the already encrypted original instruction as
-- the first user turn. The application will attach conversation metadata when
-- a later message arrives.
INSERT INTO agent_sessions (organization_id, task_id, status, current_turn, created_at, updated_at)
SELECT t.organization_id, t.id,
CASE WHEN t.status IN (
'completed', 'cancelled', 'parse_failed', 'parse_blocked', 'blocked',
'failed', 'dry_run', 'reconciliation_pending', 'execution_uncertain',
'saved_unverified', 'operation_blocked'
) THEN 'closed' ELSE 'pending' END,
1,
t.created_at,
t.updated_at
FROM tasks t
WHERE NOT EXISTS (
SELECT 1 FROM agent_sessions s WHERE s.task_id = t.id
);
INSERT INTO agent_session_messages
(organization_id, agent_session_id, task_id, turn_no, role, content_ciphertext, status, created_at, sent_at)
SELECT t.organization_id, s.id, t.id, 1, 'user', t.original_text_ciphertext,
CASE
WHEN t.status = 'parse_queued' THEN 'queued'
WHEN t.status = 'parse_running' THEN 'sending'
ELSE 'sent'
END,
t.created_at,
CASE WHEN t.status IN ('parse_queued', 'parse_running') THEN NULL ELSE t.created_at END
FROM tasks t
JOIN agent_sessions s ON s.task_id = t.id
WHERE t.original_text_ciphertext IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM agent_session_messages m
WHERE m.agent_session_id = s.id
AND m.turn_no = 1
AND m.role = 'user'
);