diff --git a/server/internal/handler/daemon.go b/server/internal/handler/daemon.go index da5ed0bcf..d9a9b18ae 100644 --- a/server/internal/handler/daemon.go +++ b/server/internal/handler/daemon.go @@ -16,6 +16,7 @@ import ( "time" "github.com/go-chi/chi/v5" + "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/multica-ai/multica/server/internal/analytics" @@ -4381,7 +4382,18 @@ func (h *Handler) ReportTaskMessages(w http.ResponseWriter, r *http.Request) { } } - for _, msg := range req.Messages { + messageIDs := make([]pgtype.UUID, len(req.Messages)) + for i := range messageIDs { + id, err := uuid.NewV7() + if err != nil { + slog.Error("failed to generate task message id", "task_id", taskID, "error", err) + writeError(w, http.StatusInternalServerError, "failed to persist task message") + return + } + messageIDs[i] = pgtype.UUID{Bytes: [16]byte(id), Valid: true} + } + + for i, msg := range req.Messages { // Redact sensitive information before persisting or broadcasting. msg.Content = redact.Text(msg.Content) msg.Output = redact.Text(msg.Output) @@ -4409,6 +4421,7 @@ func (h *Handler) ReportTaskMessages(w http.ResponseWriter, r *http.Request) { inputJSON, _ = json.Marshal(msg.Input) } created, createErr := h.Queries.CreateTaskMessage(r.Context(), db.CreateTaskMessageParams{ + ID: messageIDs[i], TaskID: parseUUID(taskID), Seq: int32(msg.Seq), Type: msg.Type, diff --git a/server/internal/handler/task_payload_nul_test.go b/server/internal/handler/task_payload_nul_test.go index 4cca36b34..b795b7573 100644 --- a/server/internal/handler/task_payload_nul_test.go +++ b/server/internal/handler/task_payload_nul_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/go-chi/chi/v5" + "github.com/google/uuid" "github.com/multica-ai/multica/server/internal/util" db "github.com/multica-ai/multica/server/pkg/db/generated" ) @@ -198,6 +199,41 @@ func TestReportTaskMessagesCallbackWithNULSucceeds(t *testing.T) { } } +func TestReportTaskMessagesCreatesUUIDv7(t *testing.T) { + ctx := context.Background() + _, taskID := seedNULTask(t, "uuidv7-messages-agent") + + w := httptest.NewRecorder() + req := daemonTaskRequest(t, "/api/daemon/tasks/"+taskID+"/messages", taskID, map[string]any{ + "messages": []any{ + map[string]any{ + "seq": 1, + "type": "text", + "content": "UUIDv7 task message", + }, + }, + }) + + testHandler.ReportTaskMessages(w, req) + if w.Code != http.StatusOK { + t.Fatalf("ReportTaskMessages returned %d, want 200: %s", w.Code, w.Body.String()) + } + + var rawID string + if err := testPool.QueryRow(ctx, ` + SELECT id::text + FROM task_message WHERE task_id = $1 AND seq = 1`, taskID).Scan(&rawID); err != nil { + t.Fatalf("read persisted task message id: %v", err) + } + messageID, err := uuid.Parse(rawID) + if err != nil { + t.Fatalf("parse task message id %q: %v", rawID, err) + } + if got := messageID.Version(); got != 7 { + t.Fatalf("task message UUID version = %d, want 7", got) + } +} + // TestPostgresRejectsUnsanitizedTaskPayloads is the control for both tests // above: it proves the database really does refuse these payloads, so the // handler tests are demonstrating a fix rather than a no-op. If PostgreSQL ever diff --git a/server/pkg/db/generated/task_message.sql.go b/server/pkg/db/generated/task_message.sql.go index 87a368c65..4e37f1224 100644 --- a/server/pkg/db/generated/task_message.sql.go +++ b/server/pkg/db/generated/task_message.sql.go @@ -12,12 +12,13 @@ import ( ) const createTaskMessage = `-- name: CreateTaskMessage :one -INSERT INTO task_message (task_id, seq, type, tool, content, input, output) -VALUES ($1, $2, $3, $4, $5, $6, $7) +INSERT INTO task_message (id, task_id, seq, type, tool, content, input, output) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, task_id, seq, type, tool, content, input, output, created_at ` type CreateTaskMessageParams struct { + ID pgtype.UUID `json:"id"` TaskID pgtype.UUID `json:"task_id"` Seq int32 `json:"seq"` Type string `json:"type"` @@ -29,6 +30,7 @@ type CreateTaskMessageParams struct { func (q *Queries) CreateTaskMessage(ctx context.Context, arg CreateTaskMessageParams) (TaskMessage, error) { row := q.db.QueryRow(ctx, createTaskMessage, + arg.ID, arg.TaskID, arg.Seq, arg.Type, diff --git a/server/pkg/db/queries/task_message.sql b/server/pkg/db/queries/task_message.sql index 3bbd3ee4f..368958dfd 100644 --- a/server/pkg/db/queries/task_message.sql +++ b/server/pkg/db/queries/task_message.sql @@ -1,6 +1,6 @@ -- name: CreateTaskMessage :one -INSERT INTO task_message (task_id, seq, type, tool, content, input, output) -VALUES ($1, $2, $3, $4, $5, $6, $7) +INSERT INTO task_message (id, task_id, seq, type, tool, content, input, output) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: ListTaskMessages :many