[Bug] Xoá agent thất bại — unique index uq_vault_docs_agent_team_scope_path va chạm khi orphan vault docs sang scope shared
Mô tả vấn đề
Khi xoá một agent có vault documents, RPC agents.delete trả về lỗi 500:
Error deleting agent: gateway error (500): failed to delete agent: internal error
Log gateway:
level=ERROR msg=agents.delete id=019e5e1c-…-18d288d10167
error="ERROR: duplicate key value violates unique constraint
\"uq_vault_docs_agent_team_scope_path\" (SQLSTATE 23505)"
Agent không thể xoá được bằng bất kỳ đường nào (Web UI, CLI goclaw agent delete --force, RPC). Toàn bộ transaction bị rollback.
Đây là hệ quả trực tiếp của cách fix #1077 (orphan vault docs sang scope='shared' thay vì personal). Fix đó giải quyết được CHECK constraint 23514, nhưng đẩy mọi doc mồ côi vào chung một không gian khoá và tạo ra va chạm UNIQUE 23505.
Nguyên nhân gốc
Ba thay đổi trong migrations/000046_vault_nullable_agent_id.up.sql kết hợp lại tạo ra va chạm không thể tránh:
DELETE FROM agents WHERE id = $1
↓
(2) FK: vault_documents.agent_id … ON DELETE SET NULL
→ PostgreSQL SET agent_id = NULL
↓
(4) Trigger trg_vault_docs_agent_null_scope:
→ NEW.scope := 'shared'
↓
(3) UNIQUE INDEX uq_vault_docs_agent_team_scope_path:
(tenant_id, COALESCE(agent_id,'000…0'), COALESCE(team_id,'000…0'), scope, path)
→ agent_id NULL bị COALESCE về CÙNG một sentinel
→ mọi doc mồ côi của MỌI agent đã xoá đều rơi vào cùng key space
↓
❌ Trùng (tenant, sentinel, sentinel, 'shared', path) → SQLSTATE 23505
Chi tiết code
migrations/000046_vault_nullable_agent_id.up.sql — cả ba mảnh nằm trong cùng một migration:
-- (2) FK: agent deletion preserves docs
ALTER TABLE vault_documents ADD CONSTRAINT vault_documents_agent_id_fkey
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL;
-- (3) Unique index COALESCE cả hai cột nullable về cùng một sentinel
CREATE UNIQUE INDEX uq_vault_docs_agent_team_scope_path
ON vault_documents (
tenant_id,
COALESCE(agent_id, '00000000-0000-0000-0000-000000000000'),
COALESCE(team_id, '00000000-0000-0000-0000-000000000000'),
scope,
path
);
-- (4) Trigger đẩy scope về 'shared'
CREATE OR REPLACE FUNCTION vault_docs_agent_null_scope_fix()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.agent_id IS NULL AND OLD.agent_id IS NOT NULL AND NEW.team_id IS NULL THEN
NEW.scope := 'shared';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
internal/gateway/methods/agents_delete.go gọi thẳng store, không xử lý vault docs trước, nên lỗi DB nổi lên nguyên si thành ErrInternal:
if err := m.agentStore.Delete(ctx, ag.ID); err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal,
i18n.T(locale, i18n.MsgFailedToDelete, "agent", fmt.Sprintf("%v", err))))
return
}
Vấn đề cốt lõi
scope='shared' + sentinel UUID biến path thành khoá duy nhất toàn tenant. Hai agent bất kỳ cùng lưu một đường dẫn (rất phổ biến: codebase/README.md, codebase/docs/…) thì agent thứ hai vĩnh viễn không xoá được, vì agent thứ nhất đã chiếm chỗ khi bị xoá trước đó.
| Trạng thái |
agent_id |
team_id |
scope |
Khoá unique hiệu dụng |
| Doc của agent A (còn sống) |
A |
NULL |
personal |
(T, A, sentinel, personal, path) |
| Sau khi xoá A |
NULL |
NULL |
shared |
(T, sentinel, sentinel, shared, path) |
Doc của agent B, cùng path |
B |
NULL |
personal |
(T, B, sentinel, personal, path) |
| Khi xoá B |
NULL |
NULL |
shared |
(T, sentinel, sentinel, shared, path) → ❌ trùng với A |
Lỗi mang tính tích luỹ: mỗi agent bị xoá lại gài thêm mìn cho các agent sau.
Các bước tái hiện
- Tạo agent A và agent B trong cùng tenant.
- Cho cả hai tạo vault document ở cùng một
path (ví dụ codebase/README.md), scope personal.
- Xoá agent A → thành công. Doc của A trở thành
agent_id=NULL, scope='shared'.
- Xoá agent B → ❌
SQLSTATE 23505 duplicate key value violates unique constraint "uq_vault_docs_agent_team_scope_path".
Truy vấn xác nhận trên hệ thống thật, trước khi xoá agent:
SELECT count(*) AS colliding_paths
FROM vault_documents w
JOIN vault_documents o
ON o.agent_id IS NULL AND o.team_id IS NULL AND o.scope = 'shared'
AND o.tenant_id = w.tenant_id AND o.path = w.path
WHERE w.agent_id = '<agent-id>';
-- → 56 (trên tổng 57 doc của agent), khớp chính xác với lỗi 23505
Đề xuất giải pháp
Phương án 1 — giữ nguyên ý định "bảo toàn dữ liệu" của #1077, khử va chạm bằng namespace (khuyến nghị).
Khi orphan, đẩy path vào một namespace riêng theo agent cũ thay vì để nguyên. Cần lưu lại agent id trước khi FK set NULL, ví dụ thêm cột orphaned_from_agent_id và mở rộng trigger:
IF NEW.agent_id IS NULL AND OLD.agent_id IS NOT NULL AND NEW.team_id IS NULL THEN
NEW.scope := 'shared';
NEW.orphaned_from_agent_id := OLD.agent_id;
NEW.path := '_archive/' || OLD.agent_id || '/' || OLD.path;
END IF;
Kèm đó nên đưa orphaned_from_agent_id vào unique index để hai agent khác nhau không bao giờ đụng nhau.
Phương án 2 — dọn vault docs trong agents.delete trước khi xoá agent.
Xử lý ở tầng ứng dụng, trong cùng transaction: xoá (hoặc re-path) vault_documents của agent rồi mới DELETE FROM agents. Ưu điểm là tôn trọng tham số deleteFiles đã có sẵn trong handler — deleteFiles=true thì xoá luôn doc, false thì mới orphan.
Phương án 3 — đổi FK sang ON DELETE CASCADE.
Đơn giản nhất nhưng xoá vĩnh viễn vault documents của agent, đi ngược lại chủ đích "agent deletion preserves docs" ghi trong migration 000046.
Ngoài ra, độc lập với phương án nào được chọn: agents.delete nên map lỗi 23505 thành thông báo có ý nghĩa thay vì internal error, để người dùng biết nguyên nhân là vault documents.
Cách khắc phục tạm thời
Xoá vault documents của agent trước, rồi xoá agent như bình thường:
DELETE FROM vault_documents WHERE agent_id = '<agent-id>';
goclaw agent delete <agent-id> --force
Lưu ý vault_links sẽ bị CASCADE theo. Nếu muốn giữ dữ liệu thì đổi path sang tiền tố riêng thay vì xoá.
Môi trường
- GoClaw
v3.15.0-beta.204 (protocol 3), image ghcr.io/nextlevelbuilder/goclaw:beta-full
- Schema version 97
- PostgreSQL
pgvector/pgvector:pg18
- Ubuntu 24.04.2 LTS, Docker Compose, network_mode host
Liên quan
[Bug] Xoá agent thất bại — unique index
uq_vault_docs_agent_team_scope_pathva chạm khi orphan vault docs sang scopesharedMô tả vấn đề
Khi xoá một agent có vault documents, RPC
agents.deletetrả về lỗi 500:Log gateway:
Agent không thể xoá được bằng bất kỳ đường nào (Web UI, CLI
goclaw agent delete --force, RPC). Toàn bộ transaction bị rollback.Đây là hệ quả trực tiếp của cách fix #1077 (orphan vault docs sang
scope='shared'thay vìpersonal). Fix đó giải quyết được CHECK constraint 23514, nhưng đẩy mọi doc mồ côi vào chung một không gian khoá và tạo ra va chạm UNIQUE 23505.Nguyên nhân gốc
Ba thay đổi trong
migrations/000046_vault_nullable_agent_id.up.sqlkết hợp lại tạo ra va chạm không thể tránh:Chi tiết code
migrations/000046_vault_nullable_agent_id.up.sql— cả ba mảnh nằm trong cùng một migration:internal/gateway/methods/agents_delete.gogọi thẳng store, không xử lý vault docs trước, nên lỗi DB nổi lên nguyên si thànhErrInternal:Vấn đề cốt lõi
scope='shared'+ sentinel UUID biếnpaththành khoá duy nhất toàn tenant. Hai agent bất kỳ cùng lưu một đường dẫn (rất phổ biến:codebase/README.md,codebase/docs/…) thì agent thứ hai vĩnh viễn không xoá được, vì agent thứ nhất đã chiếm chỗ khi bị xoá trước đó.Apersonal(T, A, sentinel, personal, path)shared(T, sentinel, sentinel, shared, path)pathBpersonal(T, B, sentinel, personal, path)shared(T, sentinel, sentinel, shared, path)→ ❌ trùng với ALỗi mang tính tích luỹ: mỗi agent bị xoá lại gài thêm mìn cho các agent sau.
Các bước tái hiện
path(ví dụcodebase/README.md), scopepersonal.agent_id=NULL, scope='shared'.SQLSTATE 23505 duplicate key value violates unique constraint "uq_vault_docs_agent_team_scope_path".Truy vấn xác nhận trên hệ thống thật, trước khi xoá agent:
Đề xuất giải pháp
Phương án 1 — giữ nguyên ý định "bảo toàn dữ liệu" của #1077, khử va chạm bằng namespace (khuyến nghị).
Khi orphan, đẩy
pathvào một namespace riêng theo agent cũ thay vì để nguyên. Cần lưu lại agent id trước khi FK set NULL, ví dụ thêm cộtorphaned_from_agent_idvà mở rộng trigger:Kèm đó nên đưa
orphaned_from_agent_idvào unique index để hai agent khác nhau không bao giờ đụng nhau.Phương án 2 — dọn vault docs trong
agents.deletetrước khi xoá agent.Xử lý ở tầng ứng dụng, trong cùng transaction: xoá (hoặc re-path)
vault_documentscủa agent rồi mớiDELETE FROM agents. Ưu điểm là tôn trọng tham sốdeleteFilesđã có sẵn trong handler —deleteFiles=truethì xoá luôn doc,falsethì mới orphan.Phương án 3 — đổi FK sang
ON DELETE CASCADE.Đơn giản nhất nhưng xoá vĩnh viễn vault documents của agent, đi ngược lại chủ đích "agent deletion preserves docs" ghi trong migration 000046.
Ngoài ra, độc lập với phương án nào được chọn:
agents.deletenên map lỗi23505thành thông báo có ý nghĩa thay vìinternal error, để người dùng biết nguyên nhân là vault documents.Cách khắc phục tạm thời
Xoá vault documents của agent trước, rồi xoá agent như bình thường:
Lưu ý
vault_linkssẽ bị CASCADE theo. Nếu muốn giữ dữ liệu thì đổipathsang tiền tố riêng thay vì xoá.Môi trường
v3.15.0-beta.204(protocol 3), imageghcr.io/nextlevelbuilder/goclaw:beta-fullpgvector/pgvector:pg18Liên quan
vault_docs_team_null_scope_fix, CHECK 23514). Bug hiện tại là mặt còn lại của cùng thiết kế: bên team vướng CHECK constraint, bên agent vướng UNIQUE index.