ca243a2d50
executus CI / test (push) Failing after 24s
Batteries-included agent-harness base, extracted from mort's agent layer. This first cut establishes the module + the zero-coupling core primitives: - lane, dispatchguard, pendingattach, run/progress.go: moved verbatim from mort - config: host config Source seam + env-var default (nil-safe helpers) - deliver: output-egress seam + Discard/Stdout defaults - identity: AdminPolicy + MemberResolver seams (nil-safe) - fanout: programmatic N×M swarm (bounded global + per-key concurrency) - README/CLAUDE.md with the vibe-coded banner; CI with Go gates + the "core stays majordomo+stdlib only" invariant Core builds with stdlib only today; majordomo enters at P1 (model/structured). go build/vet/test -race all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
1.7 KiB
Go
55 lines
1.7 KiB
Go
// 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
|
|
}
|