Skip to content

feat(api): message labels — PATCH + ?labels filter + SDKs/CLI/MCP#173

Open
jiashuoz wants to merge 2 commits into
mainfrom
feat/message-labels
Open

feat(api): message labels — PATCH + ?labels filter + SDKs/CLI/MCP#173
jiashuoz wants to merge 2 commits into
mainfrom
feat/message-labels

Conversation

@jiashuoz
Copy link
Copy Markdown
Member

Summary

Adds per-message labels: a single labels TEXT[] column on messages, a PATCH /api/v1/agents/{email}/messages/{id} endpoint with delta semantics (add_labels / remove_labels), and an AND-match filter ?labels=urgent&labels=follow-up on the existing list endpoint. Strings, not IDs — AgentMail-shaped, not Gmail-shaped. Reserved e2a: prefix for future server-applied system labels.

Slices

Slice A — Backend Go end-to-end (commit 16442c1)

  • Migration 020: labels TEXT[] NOT NULL DEFAULT '{}' + GIN index. Metadata-only on Postgres 11+ (constant default), safe on prod-sized messages.
  • identity.ModifyMessageLabels — atomic UPDATE with set semantics (union ∪ add) \ remove. Pre-checks the post-add count against MaxLabelsPerMessage (100) and returns a typed ErrLabelLimitExceeded. Cross-agent and missing rows return ErrMessageNotFound.
  • PATCH /messages/{id} handler with label validation (lowercase, [a-z0-9:_-]+, ≤64 chars, ≤50 per op, e2a:* rejected on user writes — read OK).
  • ?labels=...&labels=... AND-match filter on GET /messages. Cursor encodes the filter so tokens can't be replayed across different label sets.
  • MessageSummary.Labels and MessageDetail.Labels — always present (never null), empty array for unlabelled rows.
  • 17 tests (7 storage + 10 handler).

Slice B — Client surfaces (commit c313267)

  • TS SDK: api.updateMessageLabels, client.updateMessageLabels(...), listMessages({ labels }).
  • Python SDK: regen types.
  • CLI: e2a labels <msg-id> [--add <label> ...] [--remove <label> ...].
  • MCP: update_message_labels tool, list_messages accepts labels[]. Tool registry tests updated + positive plumbing test.

Design choices

Why
Strings, not IDs LLM agents emit categories on the fly; "create label first, then use ID" is friction. AgentMail did the same.
Delta semantics (add_labels/remove_labels), not full replace Industry convention (Gmail, AgentMail); concurrent agents don't clobber.
AND-match list filter, not OR More useful for narrowing. Repeat the query to get OR via union client-side.
Reserved e2a: prefix Future system labels (e2a:auto-tagged, e2a:hitl-approved) without collision.
100 per message, 50 per op Modeled on Gmail's 100. Generous enough for LLM batch tagging, bounded enough that GIN containment stays cheap.
Single TEXT[] column, no join Atomic update, fast GIN lookup, simpler API. Label-as-resource (Gmail) needs color/visibility metadata we don't want.

Verification

  • internal/identity: 7 new tests + full suite green
  • internal/agent: 10 new tests + full suite green
  • TS SDK: 85 tests pass
  • CLI: 100 tests pass
  • MCP: 90 tests pass (+1 new label plumbing test)

Test plan

  • Live e2e: seed DB, PATCH labels, GET to confirm round-trip, list with ?labels= filter
  • Confirm migration 020 applies cleanly on a populated dev DB (make migrate)
  • Manual: `e2a labels --add urgent --remove unread` against a real agent

🤖 Generated with Claude Code

jiashuoz and others added 2 commits May 27, 2026 23:31
Slice A of the labels feature: backend end-to-end. Strings, not IDs;
delta semantics; reserved e2a:* prefix for server-applied system labels.

Adds:
- migration 020: labels TEXT[] NOT NULL DEFAULT '{}' on messages, plus
  a GIN index. ALTER ADD COLUMN with a constant default is metadata-only
  on Postgres 11+, safe on the large prod messages table.
- identity.ModifyMessageLabels(msgID, agentID, add, remove) — single
  atomic UPDATE with set semantics (union ∪ add) \ remove. Pre-checks
  the post-add count against MaxLabelsPerMessage (100) and returns a
  typed ErrLabelLimitExceeded; cross-agent / missing rows return
  ErrMessageNotFound.
- PATCH /api/v1/agents/{email}/messages/{id} accepts
  { add_labels, remove_labels }. Returns the post-update label set so
  callers can echo state without a fetch.
- GET /api/v1/agents/{email}/messages?labels=urgent&labels=follow-up
  AND-matches via @>. Cursor encodes the filter so a token issued with
  one label set can't be replayed against a different one.
- MessageSummary and MessageDetail responses now include labels[],
  always present (never null) — empty array for unlabelled rows.
- Validation: lowercase, [a-z0-9:_-]+, ≤64 chars, ≤50 per op,
  ≤100 per message, e2a:* reserved for system writes (filter still
  permits reading them).

Tests (17):
- internal/identity/labels_test.go (7): round-trip, remove + overlap
  semantics, cap rejection, NotFound, cross-agent isolation, AND-match
  filter, never-null contract on freshly-created rows.
- internal/agent/labels_api_test.go (10): happy add/remove with
  lowercasing, system-prefix rejection, charset rejection (space,
  slash, unicode, empty), over-length rejection, per-op cap rejection,
  404 on missing, 404 on cross-agent, 401, list filter AND match +
  always-non-null labels in response shape, list filter invalid char.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice B of the labels feature: client surfaces. Wires the PATCH
endpoint + ?labels filter through every consumer.

- Generated: web/public/openapi.yaml, TS types, Python types (regen).
- TS SDK: api.updateMessageLabels(email, msgId, body) — raw HTTP.
  client.updateMessageLabels(msgId, {addLabels, removeLabels, agentEmail})
  — high-level. api.listMessages + client.listMessages accept
  `labels: string[]` and emit repeated ?labels= query params.
- CLI: `e2a labels <msg-id> [--add <label> ...] [--remove <label> ...]`
  with --agent override. Prints the post-update label set.
- MCP: new `update_message_labels` tool. `list_messages` accepts
  `labels[]` for AND-match filtering. Tool registry test updated to
  expect the new tool name in both stdio and http servers; positive
  test asserts add/remove plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant