// Package pendingattach holds the canonical pending-attachment row type // and its dedupe helper. It imports nothing from the rest of mort so // every consumer (the skills storage layer, the send_attachments tool, // and all delivery drainers) can depend on it without import cycles. // // CONTRACT: enqueue (Storage.AddPendingAttachment) is NOT idempotent — a // model that calls send_attachments more than once for the same file // leaves multiple rows. Every consumer that drains pending attachments // MUST call Dedupe before delivery, or the artifact double-posts. package pendingattach import "time" // Attachment is one deferred-attachment row. Field-for-field the shape // the skills storage layer persists. type Attachment struct { ID string RunID string SkillID string FileID string Filename string Mime string SizeBytes int64 MessageText string HostedURL string Ord int // CreatedAt is the enqueue time. Used only by the retention sweeper // (PurgePendingAttachments) and storage round-trip tests; delivery // drainers ignore it. Zero on the enqueue path — the storage layer // defaults it to time.Now(). CreatedAt time.Time } // Dedupe removes rows whose FileID has already been seen (first // occurrence wins, preserving input order). Rows with an empty FileID // are never collapsed. func Dedupe(rows []Attachment) []Attachment { if len(rows) < 2 { return rows } seen := make(map[string]struct{}, len(rows)) out := make([]Attachment, 0, len(rows)) for _, row := range rows { if row.FileID != "" { if _, dup := seen[row.FileID]; dup { continue } seen[row.FileID] = struct{}{} } out = append(out, row) } return out }