diff --git a/api/openapi.yaml b/api/openapi.yaml index 5c9959b6..d1bdc0b9 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1229,7 +1229,7 @@ components: additionalProperties: true properties: code: - description: "Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status." + description: "Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, message_not_yet_delivered, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), message_not_yet_delivered (409, retry-able — the reply/forward target is an outbound message still queued for provider submission; replies cannot thread until the provider assigns the source a Message-ID, and forwards require the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status." type: string x-e2a-error-contracts: address_in_trash: @@ -1407,6 +1407,11 @@ components: retryable: false statuses: - 409 + message_not_yet_delivered: + family: state + retryable: true + statuses: + - 409 method_not_allowed: family: server retryable: false @@ -4713,7 +4718,7 @@ paths: - messages /v1/agents/{email}/messages/{id}/forward: post: - description: "Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large." + description: "Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large." operationId: forwardMessage parameters: - in: path @@ -4787,7 +4792,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - description: "Conflict — code idempotency_in_flight: a request with this Idempotency-Key is still executing. Retry-able: wait for the first request to finish, then retry with the SAME key and byte-identical body — the retry replays the first request's response instead of re-executing the side effect." + description: "Conflict — branch on error.code; both codes are retry-able. idempotency_in_flight: a request with this Idempotency-Key is still executing — wait for the first request to finish, then retry with the SAME key and byte-identical body to replay its response. message_not_yet_delivered: the referenced message is one this agent sent that is still queued for provider submission. A reply cannot thread until the provider assigns the source a Message-ID; a forward requires the source message to have actually been sent. Retry once it reaches status=sent (poll GET /v1/messages/{id} or await the email.sent event), or send the original with wait=sent so it is terminal before you reply to or forward it." headers: X-Request-Id: $ref: "#/components/headers/XRequestID" @@ -4836,7 +4841,7 @@ paths: - messages /v1/agents/{email}/messages/{id}/reply: post: - description: "Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large." + description: "Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large." operationId: replyToMessage parameters: - in: path @@ -4910,7 +4915,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - description: "Conflict — code idempotency_in_flight: a request with this Idempotency-Key is still executing. Retry-able: wait for the first request to finish, then retry with the SAME key and byte-identical body — the retry replays the first request's response instead of re-executing the side effect." + description: "Conflict — branch on error.code; both codes are retry-able. idempotency_in_flight: a request with this Idempotency-Key is still executing — wait for the first request to finish, then retry with the SAME key and byte-identical body to replay its response. message_not_yet_delivered: the referenced message is one this agent sent that is still queued for provider submission. A reply cannot thread until the provider assigns the source a Message-ID; a forward requires the source message to have actually been sent. Retry once it reaches status=sent (poll GET /v1/messages/{id} or await the email.sent event), or send the original with wait=sent so it is terminal before you reply to or forward it." headers: X-Request-Id: $ref: "#/components/headers/XRequestID" diff --git a/docs/api.md b/docs/api.md index ca0ef989..b9ed40ce 100644 --- a/docs/api.md +++ b/docs/api.md @@ -127,6 +127,7 @@ or the state first); `rate_limited`, `idempotency_in_flight`, and 5xx | `address_in_trash` | 409 | The requested agent address is reserved by a soft-deleted agent; restore or permanently delete it first. | | `message_held` | 409 | The message is held for review and cannot perform the requested operation until the hold is resolved. | | `message_not_pending` | 409 | The review hold was already resolved (approved/rejected/expired). | +| `message_not_yet_delivered` | 409 | Reply/forward target is an outbound message still queued for provider submission. A reply cannot thread until the provider assigns its Message-ID; a forward requires the source message to have actually been sent. Retry-able: retry once it is sent, or use `wait=sent` on the original send. | | `not_in_trash` | 409 | Restore or permanent-delete was requested for a resource that is not currently in trash. | | `send_in_progress` | 409 | The message send is already executing; wait for its terminal outcome. | | `webhook_disabled` | 409 | Operation requires an enabled webhook. | diff --git a/internal/e2e/reply_unsubmitted_parent_e2e_test.go b/internal/e2e/reply_unsubmitted_parent_e2e_test.go new file mode 100644 index 00000000..98878f56 --- /dev/null +++ b/internal/e2e/reply_unsubmitted_parent_e2e_test.go @@ -0,0 +1,193 @@ +//go:build integration + +package e2e_test + +import ( + "context" + "encoding/json" + "net/url" + "regexp" + "strings" + "testing" + "time" + + "github.com/tokencanopy/e2a/internal/testutil" +) + +// asyncSendURL is sendURL without ?wait=sent: it returns the accept-time result +// (202 / status=accepted) instead of blocking until the send is terminal, which +// is what lets this test observe a parent mid-window. +func asyncSendURL(base, agentEmail string) string { + return base + "/v1/agents/" + url.PathEscape(agentEmail) + "/messages" +} + +// errEnvelope is the /v1 error envelope, narrowed to the branchable code. +type errEnvelope struct { + Err struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +func errCodeOf(t *testing.T, body []byte) string { + t.Helper() + var e errEnvelope + if err := json.Unmarshal(body, &e); err != nil { + t.Fatalf("parse error envelope %q: %v", body, err) + } + return e.Err.Code +} + +// waitForProviderID polls the message row until the send worker has recorded the +// provider-assigned Message-ID, then returns it. A bounded poll on the state the +// worker produces — not a sleep — so it is deterministic in both directions: +// it cannot pass early, and it fails loudly rather than racing. +// +// Reads via GetRepliableMessage because that is the query the reply/forward +// guard itself consults (and the only one carrying provider_message_id + +// method + delivery_status together) — so the test asserts on exactly the row +// view the handler sees. +func waitForProviderID(t *testing.T, ts *testutil.E2ATestServer, msgID string) string { + t.Helper() + ctx := context.Background() + deadline := time.Now().Add(20 * time.Second) + for { + m, err := ts.Store.GetRepliableMessage(ctx, msgID) + if err != nil { + t.Fatalf("load message %s: %v", msgID, err) + } + if m.ProviderMessageID != "" { + return m.ProviderMessageID + } + if time.Now().After(deadline) { + t.Fatalf("message %s still has no provider_message_id after 20s (delivery_status=%q)", + msgID, m.DeliveryStatus) + } + time.Sleep(25 * time.Millisecond) + } +} + +// TestReplyToUnsubmittedOutboundParentE2E pins the accept→submit window on the +// async outbound pipeline: the interval where the agent's own outbound parent is +// durably accepted (delivery_status='accepted') but the River send worker has +// not yet submitted it to SES, so provider_message_id — the ONLY id an outbound +// parent threads off (identity.Message.ThreadMessageID) — is still empty. +// +// Replies are composed once, at accept time. Inside that window +// ThreadMessageID() returns "", so a reply's In-Reply-To/References would be +// omitted from its raw bytes PERMANENTLY and the recipient's client would fork a +// new thread. The row's `status` column reads 'sent' the whole time (that column +// is the review/hold axis, not delivery), so nothing else fails closed here. +// The window is sub-second in the happy path but widens to minutes/hours during +// a provider outage, when accepted messages sit out the retry horizon. +// +// The guard turns that silent fork into a retriable 409, and this test proves +// both halves of the contract: +// +// phase 1 — parent unsubmitted → reply AND forward return 409 +// message_not_yet_delivered (nothing is silently forked); +// phase 2 — same parent, once actually submitted → reply returns 200 and its +// threading headers anchor on the parent's qualified +// provider_message_id (the guard is a narrow window, not a +// regression of reply-to-own-outbound). +// +// Determinism: the server is built WithManualJobs, so the River client is never +// started during phase 1 — the send worker has no producer and CANNOT run. No +// sleeps, no races. Phase 2 starts it and polls for the state it produces. +func TestReplyToUnsubmittedOutboundParentE2E(t *testing.T) { + pool := testutil.TestDB(t) + // Same fake-SES shape as TestReplyThreadingSESMessageIDE2E: bare id in the + // 250 response, qualified against the message_id_domain override. + fakeSMTP, _ := testutil.FakeSMTPServer(t) + ts := testutil.TestServer(t, pool, + testutil.WithOutboundSMTP(fakeSMTP.Host, fakeSMTP.Port, "test.e2a.dev"), + testutil.WithOutboundSMTPMessageIDDomain("us-east-2.amazonses.com"), + testutil.WithManualJobs()) + _, key, agent := setupDomainAndAgent(t, ts, "agent@window.example.com", "window.example.com", "", "") + ctx := context.Background() + + // (1) Send without wait=sent. The accept-tx commits the row + the send job; + // the worker is not running, so the row stays 'accepted' with no provider id. + status, body := authedJSON(t, "POST", asyncSendURL(ts.HTTPServer.URL, agent.EmailAddress()), key.PlaintextKey, + `{"to":["alice@gmail.com"],"subject":"Kickoff","text":"first touch"}`) + if status != 202 { + t.Fatalf("send status=%d body=%s, want 202 (accepted)", status, body) + } + var accepted threadSendResp + if err := json.Unmarshal(body, &accepted); err != nil { + t.Fatalf("parse send response %q: %v", body, err) + } + if accepted.Status != "accepted" { + t.Fatalf("send status=%q, want accepted", accepted.Status) + } + if accepted.ProviderMessageID != "" { + t.Fatalf("accepted send already has provider_message_id %q — the window did not open", + accepted.ProviderMessageID) + } + + // Pin the window in the DB: this is the exact state the guard must catch — + // an external ('smtp', not 'loopback') outbound, accepted, no provider id. + parent, err := ts.Store.GetRepliableMessage(ctx, accepted.MessageID) + if err != nil { + t.Fatalf("load parent row: %v", err) + } + if parent.DeliveryStatus != "accepted" || parent.ProviderMessageID != "" || parent.Method == "loopback" { + t.Fatalf("parent row = {delivery_status:%q provider_message_id:%q method:%q}, want {accepted, \"\", non-loopback}", + parent.DeliveryStatus, parent.ProviderMessageID, parent.Method) + } + + // (2) Reply inside the window → retriable 409, not a silently forked thread. + status, body = authedJSON(t, "POST", + subResource(ts.HTTPServer.URL, agent.EmailAddress(), accepted.MessageID, "reply"), + key.PlaintextKey, `{"text":"second touch"}`) + if status != 409 { + t.Fatalf("reply to unsubmitted parent status=%d body=%s, want 409", status, body) + } + if code := errCodeOf(t, body); code != "message_not_yet_delivered" { + t.Errorf("reply error code = %q, want message_not_yet_delivered (body=%s)", code, body) + } + + // (3) Forward shares the parent-resolution seam, so it must fire identically. + status, body = authedJSON(t, "POST", + subResource(ts.HTTPServer.URL, agent.EmailAddress(), accepted.MessageID, "forward"), + key.PlaintextKey, `{"to":["bob@gmail.com"],"text":"fyi"}`) + if status != 409 { + t.Fatalf("forward of unsubmitted parent status=%d body=%s, want 409", status, body) + } + if code := errCodeOf(t, body); code != "message_not_yet_delivered" { + t.Errorf("forward error code = %q, want message_not_yet_delivered (body=%s)", code, body) + } + + // (4) Close the window: start the worker and wait for the submit to record + // the qualified provider id. + ts.StartJobs(t, ctx) + providerID := waitForProviderID(t, ts, accepted.MessageID) + qualified := regexp.MustCompile(`^<[^@>]+@us-east-2\.amazonses\.com>$`) + if !qualified.MatchString(providerID) { + t.Fatalf("provider_message_id = %q, want the domain-qualified on-wire form ", + providerID) + } + + // (5) The SAME reply now succeeds and threads onto the parent — proving the + // guard bounds a window rather than blocking reply-to-own-outbound. + status, body = authedJSON(t, "POST", + subResource(ts.HTTPServer.URL, agent.EmailAddress(), accepted.MessageID, "reply"), + key.PlaintextKey, `{"text":"second touch"}`) + if status != 200 { + t.Fatalf("reply after submit status=%d body=%s, want 200", status, body) + } + rep := parseThreadSend(t, body) + repMsg, err := ts.Store.GetMessageWithContent(ctx, rep.MessageID, agent.ID) + if err != nil { + t.Fatalf("load reply row: %v", err) + } + raw := string(repMsg.RawMessage) + if !strings.Contains(raw, "In-Reply-To: "+providerID) { + t.Errorf("reply In-Reply-To not anchored on the parent's on-wire Message-ID %q; raw headers=\n%s", + providerID, raw[:min(len(raw), 600)]) + } + if !strings.Contains(raw, "References: "+providerID) { + t.Errorf("reply References missing the parent's on-wire Message-ID %q; raw headers=\n%s", + providerID, raw[:min(len(raw), 600)]) + } +} diff --git a/internal/httpapi/error_catalog.go b/internal/httpapi/error_catalog.go index b6bcd00f..b17d578f 100644 --- a/internal/httpapi/error_catalog.go +++ b/internal/httpapi/error_catalog.go @@ -50,6 +50,10 @@ var errorCodeCatalog = []errorCodeContract{ {Code: "address_in_trash", Status: "409", Family: "state"}, {Code: "message_held", Status: "409", Family: "state"}, {Code: "message_not_pending", Status: "409", Family: "state"}, + // Retryable: the referenced outbound parent is queued for provider submission + // but not yet submitted, so it has no Message-ID to thread onto yet. Once the + // send worker submits, the identical reply/forward succeeds. + {Code: "message_not_yet_delivered", Status: "409", Family: "state", Retryable: true}, {Code: "not_in_trash", Status: "409", Family: "state"}, {Code: "send_in_progress", Status: "409", Family: "state"}, {Code: "webhook_disabled", Status: "409", Family: "state"}, diff --git a/internal/httpapi/errors.go b/internal/httpapi/errors.go index 9902b8fe..cf33f8cb 100644 --- a/internal/httpapi/errors.go +++ b/internal/httpapi/errors.go @@ -54,7 +54,7 @@ type ErrorEnvelope struct { // ErrorBody is the inner object of the envelope. type ErrorBody struct { - Code string `json:"code" doc:"Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status."` + Code string `json:"code" doc:"Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, message_not_yet_delivered, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), message_not_yet_delivered (409, retry-able — the reply/forward target is an outbound message still queued for provider submission; replies cannot thread until the provider assigns the source a Message-ID, and forwards require the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status."` Message string `json:"message" doc:"Human-readable explanation. Not for branching — use code."` Details any `json:"details,omitempty" doc:"Optional structured context, polymorphic by code. Treat it as an open object keyed off code; unknown codes and fields must be preserved."` RequestID string `json:"request_id" doc:"Echoes the X-Request-Id response header so a failing call is greppable in logs."` diff --git a/internal/httpapi/outbound.go b/internal/httpapi/outbound.go index 5f58f54d..e073345e 100644 --- a/internal/httpapi/outbound.go +++ b/internal/httpapi/outbound.go @@ -87,6 +87,16 @@ func (s *Server) idempotencyReuseResponse() *huma.Response { "Unprocessable — branch on error.code. idempotency_key_reuse: this Idempotency-Key was already used with a DIFFERENT request body (the dedup hash covers the route + the raw body bytes) — do NOT retry as-is; a legitimate retry must resend the byte-identical body, and a genuinely new request needs a fresh key. invalid_request: a semantic validation failure in the request body.") } +// replyForwardConflictResponse is the declared 409 for reply/forward. They honor +// Idempotency-Key like /send, so idempotency_in_flight applies — but they alone +// also resolve a PARENT message, which adds message_not_yet_delivered. Both are +// retry-able and render the same envelope, so the two codes share one declared +// 409; a client branches on error.code. +func (s *Server) replyForwardConflictResponse() *huma.Response { + return s.jsonResponse(reflect.TypeOf(ErrorEnvelope{}), "ErrorEnvelope", + "Conflict — branch on error.code; both codes are retry-able. idempotency_in_flight: a request with this Idempotency-Key is still executing — wait for the first request to finish, then retry with the SAME key and byte-identical body to replay its response. message_not_yet_delivered: the referenced message is one this agent sent that is still queued for provider submission. A reply cannot thread until the provider assigns the source a Message-ID; a forward requires the source message to have actually been sent. Retry once it reaches status=sent (poll GET /v1/messages/{id} or await the email.sent event), or send the original with wait=sent so it is terminal before you reply to or forward it.") +} + const composedMessageCeilingDoc = "Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large." // outboundPayloadTooLargeResponse documents both independent outbound size @@ -281,19 +291,19 @@ func (s *Server) registerOutbound() { huma.Register(s.API, huma.Operation{ OperationID: "replyToMessage", Method: http.MethodPost, Path: "/v1/agents/{email}/messages/{id}/reply", Summary: "Reply to a message", Tags: []string{"messages"}, - Description: "Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). " + composedMessageCeilingDoc, + Description: "Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). " + composedMessageCeilingDoc, Security: []map[string][]string{{"bearer": {}}}, MaxBodyBytes: maxOutboundBytes, - Responses: map[string]*huma.Response{"202": accepted202(), "400": badRequest400(), "402": s.limitExceededResponse(), "409": s.idempotencyInFlightResponse(), "413": s.outboundPayloadTooLargeResponse(), "422": s.idempotencyReuseResponse(), "429": s.rateLimitedResponse(), "default": s.errorEnvelopeResponse()}, + Responses: map[string]*huma.Response{"202": accepted202(), "400": badRequest400(), "402": s.limitExceededResponse(), "409": s.replyForwardConflictResponse(), "413": s.outboundPayloadTooLargeResponse(), "422": s.idempotencyReuseResponse(), "429": s.rateLimitedResponse(), "default": s.errorEnvelopeResponse()}, }, s.handleReply) huma.Register(s.API, huma.Operation{ OperationID: "forwardMessage", Method: http.MethodPost, Path: "/v1/agents/{email}/messages/{id}/forward", Summary: "Forward a message", Tags: []string{"messages"}, - Description: "Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). " + composedMessageCeilingDoc, + Description: "Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). " + composedMessageCeilingDoc, Security: []map[string][]string{{"bearer": {}}}, MaxBodyBytes: maxOutboundBytes, - Responses: map[string]*huma.Response{"202": accepted202(), "400": badRequest400(), "402": s.limitExceededResponse(), "409": s.idempotencyInFlightResponse(), "413": s.outboundPayloadTooLargeResponse(), "422": s.idempotencyReuseResponse(), "429": s.rateLimitedResponse(), "default": s.errorEnvelopeResponse()}, + Responses: map[string]*huma.Response{"202": accepted202(), "400": badRequest400(), "402": s.limitExceededResponse(), "409": s.replyForwardConflictResponse(), "413": s.outboundPayloadTooLargeResponse(), "422": s.idempotencyReuseResponse(), "429": s.rateLimitedResponse(), "default": s.errorEnvelopeResponse()}, }, s.handleForward) huma.Register(s.API, huma.Operation{ @@ -383,9 +393,56 @@ func (s *Server) loadRepliableMessage(ctx context.Context, address, msgID string if err != nil || msg == nil || msg.AgentID != ag.ID { return nil, nil, nil, NewError(http.StatusNotFound, "not_found", "message not found") } + if env := parentNotYetSubmitted(msg); env != nil { + return nil, nil, nil, env + } return ag, msg, user, nil } +// parentNotYetSubmitted returns a retriable 409 when the reply/forward target is +// the agent's own outbound message that is still queued for external submission. +// +// Threading is composed ONCE, at accept time, off the parent's RFC 5322 +// Message-ID (identity.Message.ThreadMessageID). For an outbound parent that id +// is provider_message_id — which the send worker only records when it submits to +// SES (MarkOutboundSentTx). Reply to such a parent inside that window and +// ThreadMessageID() returns "", so the reply's In-Reply-To/References are +// omitted from its raw bytes PERMANENTLY and the recipient's client forks a new +// thread. The window is normally sub-second but widens to the retry horizon +// during a provider outage, so failing closed with a retriable conflict beats +// silently forking the thread. +// +// Each clause is load-bearing; together they say "queued for external +// submission, not yet submitted": +// +// - direction == outbound — only an outbound parent threads off +// provider_message_id. Inbound parents anchor on email_message_id, which is +// written at intake and never empty-then-filled, so they can't hit this. +// - method != loopback — a self-send's outbound copy is delivered locally and +// terminally by performSelfSend (internal/agent/selfsend.go), which commits +// it with delivery_status='sent' and a synthetic loopback provider id. It is +// never awaiting external submission, so replying to it must not 409. It is +// already excluded by both clauses below; this states the intent explicitly +// rather than relying on that overlap. +// - provider_message_id == "" — the exact condition that makes +// ThreadMessageID() return "". Once populated, threading resolves and the +// reply is unaffected. +// - delivery_status in (accepted, sending) — the row is genuinely still in +// flight. This is what keeps the guard from firing on a TERMINAL send that +// never got a provider id (delivery_status='failed', e.g. SES rejected the +// submit): that parent will never gain one, so a retry cannot clear the +// condition and a 409 would strand the caller forever. +func parentNotYetSubmitted(msg *identity.Message) *ErrorEnvelope { + if msg.Direction != "outbound" || msg.Method == "loopback" || msg.ProviderMessageID != "" { + return nil + } + if msg.DeliveryStatus != "accepted" && msg.DeliveryStatus != "sending" { + return nil + } + return NewError(http.StatusConflict, "message_not_yet_delivered", + "referenced message not yet delivered — retry after it is sent, or use wait=sent on the original send") +} + func (s *Server) handleReply(ctx context.Context, in *replyInput) (*sendOutput, error) { ag, msg, user, err := s.loadRepliableMessage(ctx, in.Address, in.ID) if err != nil { diff --git a/internal/identity/store.go b/internal/identity/store.go index ae6b8196..fed31782 100644 --- a/internal/identity/store.go +++ b/internal/identity/store.go @@ -1774,15 +1774,24 @@ func (m *Message) ThreadMessageID() string { // not a legitimate reply/forward anchor. `expires_at > now()` keeps expired // rows out the same way GetInboundMessage does. Callers still scope the result // to the owning agent (id-only lookup here does not). +// +// method + delivery_status are loaded (beyond GetInboundMessage's column list) +// because `status` is the review/hold axis, not the delivery axis: an outbound +// row reads status='sent' the instant it is accepted, long before the send +// worker submits it. The reply/forward handlers need the delivery axis to tell +// an outbound that is still queued for external submission (no +// provider_message_id yet → ThreadMessageID() would return "" and silently drop +// In-Reply-To/References) from one that is genuinely terminal. See +// httpapi.parentNotYetSubmitted. func (s *Store) GetRepliableMessage(ctx context.Context, id string) (*Message, error) { m := &Message{} var authVerdict []byte err := s.pool.QueryRow(ctx, - `SELECT id, agent_id, direction, sender, recipient, to_recipients, cc, reply_to, subject, email_message_id, COALESCE(provider_message_id, ''), raw_message, auth_verdict, COALESCE(flagged, false), COALESCE(flag_reason, ''), COALESCE(conversation_id, ''), created_at, expires_at + `SELECT id, agent_id, direction, sender, recipient, to_recipients, cc, reply_to, subject, email_message_id, COALESCE(provider_message_id, ''), COALESCE(method, ''), COALESCE(delivery_status, ''), raw_message, auth_verdict, COALESCE(flagged, false), COALESCE(flag_reason, ''), COALESCE(conversation_id, ''), created_at, expires_at FROM messages WHERE id = $1 AND expires_at > now() AND deleted_at IS NULL AND status NOT IN (`+heldInboundStatuses+`)`, id, - ).Scan(&m.ID, &m.AgentID, &m.Direction, &m.Sender, &m.Recipient, &m.ToRecipients, &m.CC, &m.ReplyTo, &m.Subject, &m.EmailMessageID, &m.ProviderMessageID, &m.RawMessage, &authVerdict, &m.Flagged, &m.FlagReason, &m.ConversationID, &m.CreatedAt, &m.ExpiresAt) + ).Scan(&m.ID, &m.AgentID, &m.Direction, &m.Sender, &m.Recipient, &m.ToRecipients, &m.CC, &m.ReplyTo, &m.Subject, &m.EmailMessageID, &m.ProviderMessageID, &m.Method, &m.DeliveryStatus, &m.RawMessage, &authVerdict, &m.Flagged, &m.FlagReason, &m.ConversationID, &m.CreatedAt, &m.ExpiresAt) if err != nil { return nil, err } diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 445c4650..76082253 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -44,6 +44,7 @@ type testServerOpts struct { outboundSMTPPort int outboundSMTPFromDomain string outboundSMTPMessageIDDomain string + manualJobs bool } type TestServerOption func(*testServerOpts) @@ -71,6 +72,18 @@ func WithOutboundSMTPMessageIDDomain(domain string) TestServerOption { } } +// WithManualJobs builds the shared River client but does NOT start it, so no +// queue is worked until the test calls StartJobs. Enqueue still works (an +// InsertTx is just a row), so an accepted send durably lands its outbound_send +// job and then SITS there — which is how a test pins the accept→submit window +// (delivery_status='accepted', provider_message_id still empty) with no sleeps +// and no race: an unstarted client has no producers, so the worker CANNOT run. +// Default (unset) keeps the production-shaped behavior every other test relies +// on: the client is started and drains the queue on its own. +func WithManualJobs() TestServerOption { + return func(o *testServerOpts) { o.manualJobs = true } +} + type E2ATestServer struct { HTTPServer *httptest.Server SMTPAddr string @@ -91,6 +104,28 @@ type E2ATestServer struct { pool *pgxpool.Pool outboxWorker *webhookpub.OutboxWorker deliverWorker *webhookdelivery.DeliverWorker + + // jobsClient is the shared River client. jobsStarted tracks whether it is + // running so StartJobs is idempotent and cleanup only stops a started + // client (see WithManualJobs). + jobsClient *jobs.Client + jobsStarted bool +} + +// StartJobs starts the shared River client for a server built with +// WithManualJobs, releasing every queued job to its worker. Idempotent, and a +// no-op on a server whose client is already running. Pair it with a bounded +// poll on the state the worker produces (e.g. provider_message_id becoming +// non-empty) rather than a sleep. +func (ts *E2ATestServer) StartJobs(t *testing.T, ctx context.Context) { + t.Helper() + if ts.jobsStarted { + return + } + if err := ts.jobsClient.Start(ctx); err != nil { + t.Fatalf("start River client: %v", err) + } + ts.jobsStarted = true } // DrainAndDeliver runs the outbox drain (webhook_events → @@ -183,8 +218,11 @@ func TestServer(t *testing.T, pool *pgxpool.Pool, opts ...TestServerOption) *E2A t.Fatalf("build River client: %v", err) } outboundJobs.SetEnqueuer(jobsClient) - if err := jobsClient.Start(context.Background()); err != nil { - t.Fatalf("start River client: %v", err) + // Deferred under WithManualJobs so the test owns when queues start draining. + if !o.manualJobs { + if err := jobsClient.Start(context.Background()); err != nil { + t.Fatalf("start River client: %v", err) + } } usageStore := usage.NewStore(pool) // Generous caps — e2e exercises behavior, not quota enforcement. @@ -267,13 +305,18 @@ func TestServer(t *testing.T, pool *pgxpool.Pool, opts ...TestServerOption) *E2A pool: pool, outboxWorker: outboxWorker, deliverWorker: deliverWorker, + jobsClient: jobsClient, + jobsStarted: !o.manualJobs, } t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if err := jobsClient.Stop(ctx); err != nil { - t.Errorf("stop River client: %v", err) + // Stop only a started client — Stop on one that never started errors. + if ts.jobsStarted { + if err := jobsClient.Stop(ctx); err != nil { + t.Errorf("stop River client: %v", err) + } } httpServer.Close() smtpServer.Close() diff --git a/sdks/python/src/e2a/v1/generated/api/messages_api.py b/sdks/python/src/e2a/v1/generated/api/messages_api.py index d8bc3510..a003aec4 100644 --- a/sdks/python/src/e2a/v1/generated/api/messages_api.py +++ b/sdks/python/src/e2a/v1/generated/api/messages_api.py @@ -381,7 +381,7 @@ async def forward_message( ) -> SendResultView: """Forward a message - Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. :param email: (required) :type email: str @@ -471,7 +471,7 @@ async def forward_message_with_http_info( ) -> ApiResponse[SendResultView]: """Forward a message - Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. :param email: (required) :type email: str @@ -561,7 +561,7 @@ async def forward_message_without_preload_content( ) -> RESTResponseType: """Forward a message - Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. :param email: (required) :type email: str @@ -1791,7 +1791,7 @@ async def reply_to_message( ) -> SendResultView: """Reply to a message - Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. :param email: (required) :type email: str @@ -1881,7 +1881,7 @@ async def reply_to_message_with_http_info( ) -> ApiResponse[SendResultView]: """Reply to a message - Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. :param email: (required) :type email: str @@ -1971,7 +1971,7 @@ async def reply_to_message_without_preload_content( ) -> RESTResponseType: """Reply to a message - Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. :param email: (required) :type email: str diff --git a/sdks/python/src/e2a/v1/generated/models/error_body.py b/sdks/python/src/e2a/v1/generated/models/error_body.py index bc9887f0..ada5b2fb 100644 --- a/sdks/python/src/e2a/v1/generated/models/error_body.py +++ b/sdks/python/src/e2a/v1/generated/models/error_body.py @@ -26,7 +26,7 @@ class ErrorBody(BaseModel): """ ErrorBody """ # noqa: E501 - code: StrictStr = Field(description="Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status.") + code: StrictStr = Field(description="Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, message_not_yet_delivered, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), message_not_yet_delivered (409, retry-able — the reply/forward target is an outbound message still queued for provider submission; replies cannot thread until the provider assigns the source a Message-ID, and forwards require the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status.") details: Optional[Dict[str, Any]] = Field(default=None, description="Optional structured context, polymorphic by code. Treat it as an open object keyed off code; unknown codes and fields must be preserved.") message: StrictStr = Field(description="Human-readable explanation. Not for branching — use code.") request_id: StrictStr = Field(description="Echoes the X-Request-Id response header so a failing call is greppable in logs.") diff --git a/sdks/typescript/src/v1/generated/apis/MessagesApi.ts b/sdks/typescript/src/v1/generated/apis/MessagesApi.ts index eb3a4287..710c9974 100644 --- a/sdks/typescript/src/v1/generated/apis/MessagesApi.ts +++ b/sdks/typescript/src/v1/generated/apis/MessagesApi.ts @@ -88,7 +88,7 @@ export class MessagesApiRequestFactory extends BaseAPIRequestFactory { } /** - * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Forward a message * @param email * @param id @@ -393,7 +393,7 @@ export class MessagesApiRequestFactory extends BaseAPIRequestFactory { } /** - * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Reply to a message * @param email * @param id @@ -728,7 +728,7 @@ export class MessagesApiResponseProcessor { ObjectSerializer.parse(await response.body.text(), contentType), "ErrorEnvelope", "" ) as ErrorEnvelope; - throw new ApiException(response.httpStatusCode, "Conflict — code idempotency_in_flight: a request with this Idempotency-Key is still executing. Retry-able: wait for the first request to finish, then retry with the SAME key and byte-identical body — the retry replays the first request\'s response instead of re-executing the side effect.", body, response.headers); + throw new ApiException(response.httpStatusCode, "Conflict — branch on error.code; both codes are retry-able. idempotency_in_flight: a request with this Idempotency-Key is still executing — wait for the first request to finish, then retry with the SAME key and byte-identical body to replay its response. message_not_yet_delivered: the referenced message is one this agent sent that is still queued for provider submission. A reply cannot thread until the provider assigns the source a Message-ID; a forward requires the source message to have actually been sent. Retry once it reaches status=sent (poll GET /v1/messages/{id} or await the email.sent event), or send the original with wait=sent so it is terminal before you reply to or forward it.", body, response.headers); } if (isCodeInRange("413", response.httpStatusCode)) { const body: ErrorEnvelope = ObjectSerializer.deserialize( @@ -928,7 +928,7 @@ export class MessagesApiResponseProcessor { ObjectSerializer.parse(await response.body.text(), contentType), "ErrorEnvelope", "" ) as ErrorEnvelope; - throw new ApiException(response.httpStatusCode, "Conflict — code idempotency_in_flight: a request with this Idempotency-Key is still executing. Retry-able: wait for the first request to finish, then retry with the SAME key and byte-identical body — the retry replays the first request\'s response instead of re-executing the side effect.", body, response.headers); + throw new ApiException(response.httpStatusCode, "Conflict — branch on error.code; both codes are retry-able. idempotency_in_flight: a request with this Idempotency-Key is still executing — wait for the first request to finish, then retry with the SAME key and byte-identical body to replay its response. message_not_yet_delivered: the referenced message is one this agent sent that is still queued for provider submission. A reply cannot thread until the provider assigns the source a Message-ID; a forward requires the source message to have actually been sent. Retry once it reaches status=sent (poll GET /v1/messages/{id} or await the email.sent event), or send the original with wait=sent so it is terminal before you reply to or forward it.", body, response.headers); } if (isCodeInRange("413", response.httpStatusCode)) { const body: ErrorEnvelope = ObjectSerializer.deserialize( diff --git a/sdks/typescript/src/v1/generated/models/ErrorBody.ts b/sdks/typescript/src/v1/generated/models/ErrorBody.ts index cb99269a..b18399af 100644 --- a/sdks/typescript/src/v1/generated/models/ErrorBody.ts +++ b/sdks/typescript/src/v1/generated/models/ErrorBody.ts @@ -14,7 +14,7 @@ import { HttpFile } from '../http/http.js'; export class ErrorBody { /** - * Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status. + * Machine-branchable error code — the stable discriminator clients switch on. Open set: treat it as a string and tolerate unknown values, since new codes may be added over time (branch on the ones you handle, fall back to the HTTP status otherwise). Exact current vocabulary (machine-checked): unauthorized, forbidden, blocked_by_policy, invalid_request, invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope, reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty, recipient_suppressed, not_found, attachment_not_found, template_not_found, starter_template_not_found, gone, conflict, agent_taken, domain_taken, alias_taken, address_in_trash, message_held, message_not_pending, message_not_yet_delivered, not_in_trash, send_in_progress, webhook_disabled, webhook_cooldown, domain_not_registered, domain_has_agents, domain_not_verified, limit_exceeded, rate_limited, template_limit_reached, webhook_limit_reached, idempotency_in_flight, idempotency_key_reuse, payload_too_large, attachment_too_large, not_implemented, events_log_disabled, limits_unavailable, internal_error, method_not_allowed, unsupported_media_type, error. Grouped semantics: auth: unauthorized (401), forbidden (403), blocked_by_policy (403, outbound policy gate). Validation: invalid_request is the single canonical code for input-validation failures whether they arrive as 400 (malformed) or 422 (semantically invalid); field/resource-specific invalid_* refinements (invalid_cursor, invalid_filter, invalid_domain, invalid_slug, invalid_recipient, invalid_attachment, invalid_template, invalid_event_type, invalid_webhook_url, invalid_expires_at, invalid_scope), reserved_domain, too_many_recipients, template_render_failed, template_rendered_empty (all 400); recipient_suppressed (422). Not found: not_found (404) plus the *_not_found family (attachment_not_found, template_not_found, starter_template_not_found); gone (410, past retention). Conflict/state: conflict (409, generic), the *_taken family — the requested identifier is already claimed — (agent_taken, domain_taken, alias_taken, all 409), address_in_trash (409), message_held (409), message_not_pending (409), message_not_yet_delivered (409, retry-able — the reply/forward target is an outbound message still queued for provider submission; replies cannot thread until the provider assigns the source a Message-ID, and forwards require the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send), not_in_trash (409), send_in_progress (409), webhook_disabled (409), webhook_cooldown (409), domain_not_registered (400), domain_has_agents (400), domain_not_verified (400 on create-agent, 403 on send). Capacity: limit_exceeded (402, plan quota — see LimitExceededDetails), rate_limited (429, request rate — see RateLimitedDetails), template_limit_reached and webhook_limit_reached (400, fixed per-account caps). Idempotency: idempotency_in_flight (409, wait then retry the byte-identical request), idempotency_key_reuse (422, caller bug — do not retry as-is). Size: payload_too_large (413, request body), attachment_too_large (413, inline fetch over the cap — use download_url). Availability: not_implemented (501, feature not available on this deployment), events_log_disabled (501), limits_unavailable (503). Server/fallback: internal_error (5xx), method_not_allowed (405), unsupported_media_type (415), and the generic code error for any otherwise-unmapped status. */ 'code': string; /** diff --git a/sdks/typescript/src/v1/generated/types/ObjectParamAPI.ts b/sdks/typescript/src/v1/generated/types/ObjectParamAPI.ts index d14ea926..eb928d0c 100644 --- a/sdks/typescript/src/v1/generated/types/ObjectParamAPI.ts +++ b/sdks/typescript/src/v1/generated/types/ObjectParamAPI.ts @@ -1462,7 +1462,7 @@ export class ObjectMessagesApi { } /** - * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Forward a message * @param param the request object */ @@ -1471,7 +1471,7 @@ export class ObjectMessagesApi { } /** - * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Forward a message * @param param the request object */ @@ -1534,7 +1534,7 @@ export class ObjectMessagesApi { } /** - * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Reply to a message * @param param the request object */ @@ -1543,7 +1543,7 @@ export class ObjectMessagesApi { } /** - * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Reply to a message * @param param the request object */ diff --git a/sdks/typescript/src/v1/generated/types/ObservableAPI.ts b/sdks/typescript/src/v1/generated/types/ObservableAPI.ts index 7c008ae2..535fddb8 100644 --- a/sdks/typescript/src/v1/generated/types/ObservableAPI.ts +++ b/sdks/typescript/src/v1/generated/types/ObservableAPI.ts @@ -1237,7 +1237,7 @@ export class ObservableMessagesApi { } /** - * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Forward a message * @param email * @param id @@ -1266,7 +1266,7 @@ export class ObservableMessagesApi { } /** - * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Forward a message * @param email * @param id @@ -1413,7 +1413,7 @@ export class ObservableMessagesApi { } /** - * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Reply to a message * @param email * @param id @@ -1442,7 +1442,7 @@ export class ObservableMessagesApi { } /** - * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Reply to a message * @param email * @param id diff --git a/sdks/typescript/src/v1/generated/types/PromiseAPI.ts b/sdks/typescript/src/v1/generated/types/PromiseAPI.ts index 1e4974b6..12a84cfb 100644 --- a/sdks/typescript/src/v1/generated/types/PromiseAPI.ts +++ b/sdks/typescript/src/v1/generated/types/PromiseAPI.ts @@ -902,7 +902,7 @@ export class PromiseMessagesApi { } /** - * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Forward a message * @param email * @param id @@ -917,7 +917,7 @@ export class PromiseMessagesApi { } /** - * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Forward a message (inbound or outbound) to new recipients; the original is quoted and its attachments are carried over by default. Any attachments[] you supply are added on top of the originals. 202 when held for HITL. Forwarding a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — a forward requires the source message to have actually been sent; retry once it is sent, or use wait=sent on the original send. Attachment limits apply to the combined set (carried-over originals + supplied): at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Forward a message * @param email * @param id @@ -1030,7 +1030,7 @@ export class PromiseMessagesApi { } /** - * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Reply to a message * @param email * @param id @@ -1045,7 +1045,7 @@ export class PromiseMessagesApi { } /** - * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. + * Reply to a message (inbound or outbound); recipients and threading are derived from the original. Replying to a message the agent received targets its sender; replying to a message the agent sent continues the thread to its original recipients (`reply_all` also re-includes the original Cc). 202 when held for HITL. Replying to a message this agent sent that has not been submitted to the provider yet returns 409 message_not_yet_delivered — it has no Message-ID to thread onto; retry once it is sent, or use wait=sent on the original send. Attachment limits: at most 10 attachments, each ≤ 10 MB decoded, ≤ 25 MB decoded combined (over-count → 400 invalid_request; over-size → 413 payload_too_large). Composed-message ceiling: 10 MiB (10485760 bytes), measured as subject + text + html + decoded attachment bytes; exceeding it returns 413 payload_too_large. * Reply to a message * @param email * @param id