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>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package pendingattach
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDedupe_CollapsesByFileID(t *testing.T) {
|
||||
rows := []Attachment{
|
||||
{ID: "1", FileID: "a", Ord: 0},
|
||||
{ID: "2", FileID: "b", Ord: 1},
|
||||
{ID: "3", FileID: "a", Ord: 2}, // dup of file a
|
||||
}
|
||||
out := Dedupe(rows)
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("want 2 rows, got %d: %+v", len(out), out)
|
||||
}
|
||||
if out[0].ID != "1" || out[1].ID != "2" {
|
||||
t.Fatalf("first-wins order not preserved: %+v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupe_EmptyFileIDNeverCollapsed(t *testing.T) {
|
||||
rows := []Attachment{
|
||||
{ID: "1", FileID: ""},
|
||||
{ID: "2", FileID: ""},
|
||||
}
|
||||
if got := Dedupe(rows); len(got) != 2 {
|
||||
t.Fatalf("empty FileID rows must not collapse, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupe_ShortInputPassthrough(t *testing.T) {
|
||||
rows := []Attachment{{ID: "1", FileID: "a"}}
|
||||
if got := Dedupe(rows); len(got) != 1 {
|
||||
t.Fatalf("single row should pass through, got %d", len(got))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user