Skip to content

release: corte de agosto — develop para main (12 commits) - #10

Merged
gomessguii merged 12 commits into
mainfrom
develop
Aug 28, 2026
Merged

release: corte de agosto — develop para main (12 commits)#10
gomessguii merged 12 commits into
mainfrom
develop

Conversation

@gomessguii

@gomessguii gomessguii commented Aug 28, 2026

Copy link
Copy Markdown
Member

Corte de release: leva a develop para a main12 commits, 4 cards.

Faz parte da cascata que leva o evolution-ecosystem para produção, parada em 2 de julho. A main do superprojeto aponta para SHAs que estão na main de cada submódulo, então cada repositório corta primeiro; o superprojeto bumpa os ponteiros por último.

Merge testado com git merge-tree antes de abrir: sem conflitos.

Este repositório não depende dos demais dentro da cascata — pode ser mergeado em qualquer ordem em relação aos irmãos.

As migrations do conjunto foram ensaiadas contra uma cópia restaurada do banco de produção (Postgres 18.6, imagens por digest): cinco passos do migrate-job, todos exit 0, dado intacto, idempotência confirmada.

Runbook: https://claude.ai/code/artifact/3a9608d8-4126-4c9c-9934-8e0e9074fbff

Summary by Sourcery

Improve pipeline resilience and multimodal message handling while adding automated validation and safer production defaults.

New Features:

  • Forward incoming media attachments through the debounced pipeline to the AI Processor as A2A file parts.
  • Retry transient AI Processor failures with configurable backoff while preserving text responses when media downloads fail.
  • Notify customers when AI processing fails, with configurable messaging and opt-out support.

Bug Fixes:

  • Require BOT_RUNTIME_SECRET at startup to prevent unauthenticated requests.
  • Prevent unauthorized or unsafe media URL fetching through host allowlisting and redirect validation.
  • Increase the default AI call timeout to support longer tool-calling turns.

Enhancements:

  • Add bounded media download budgets, MIME validation, and best-effort handling for oversized or invalid attachments.
  • Persist and aggregate attachments in Redis alongside debounced text, clearing both together.

CI:

  • Add GitHub Actions CI to build, vet, and test the Go project with a Redis service.

Deployment:

  • Expose AI retry and media allowlist configuration through environment variables and Kubernetes configuration.

Tests:

  • Add coverage for AI retries, attachment forwarding and limits, SSRF protections, failure notices, Redis attachment buffering, and end-to-end pipeline behavior.

Matheus Pastorini and others added 12 commits July 17, 2026 21:39
The bot-runtime made a single call to the AI Processor: any non-200 (401/5xx) or
network error aborted the pipeline and the customer's message was dropped with no
reply and no retry (only AI_CALL_TIMEOUT_SECONDS existed). A momentary blip —
deploy, restart, DB hiccup — meant a permanently lost answer.

- ai_adapter.go: extract a single attempt into doOnce() and wrap Call() in a retry
  loop with exponential backoff + jitter. Retryable: network errors and
  429/500/502/503/504. NOT retried: 4xx (permanent), per-attempt timeout, pipeline
  cancellation. Body is built once and reused per attempt.
- config.go: AI_CALL_MAX_RETRIES (default 2) and AI_CALL_RETRY_BASE_MS (default 200).
- main.go: wire the new config into NewAIAdapter.
- tests: 503->200 retry succeeds (2 calls); persistent 500 exhausts retries
  (1+2 calls); 400 not retried (1 call); network error then success (2 calls);
  maxRetries=0 disables retry. Existing tests updated to the new signature (0 retries).

Complements EVO-2166: the processor now returns 503 (not a silent 401) on infra
errors, so this retry covers the transient auth/infra case. Root cause of the
incident is EVO-2141 (pool_pre_ping, already merged); this is defense in depth.

Note: test/e2e/e2e_test.go was already incompatible with NewAIAdapter on develop
(pre-existing, unrelated) and is left as-is; repo CI is docker-only (no go test lane).
Review follow-ups on the AI Processor retry path:

- Add an overall time-budget backstop so the retry loop is provably bounded ((attempts+1) x per-attempt timeout + summed max backoff); the +1 slack keeps a per-attempt timeout surfacing as ErrAITimeout instead of being swallowed by the backstop. AC "teto de tempo total".

- Add TestCall_TimeoutIsNotRetried: a per-attempt timeout must return ErrAITimeout and must NOT be retried with retries enabled. AC #5 "timeout por tentativa".

- Document the idempotency contract on the retry path (502/504/network replay can re-run an already-processed turn; customer still gets one reply; dedupe of the duplicate server-side turn is the AI Processor's job, tracked in EVO-2166).
…ssor-retry

fix(EVO-2167): retry the AI Processor call on transient failures
…e parts

The bot only ever sent a text part, so images/audio the customer sent never
reached the AI (the agent replied "No content to process"). Accept attachments on
the inbound event, carry them through the debounce window, download each and send
it as a base64 A2A file part. Part of EVO-2178 (image end-to-end).

- pipeline/model: MessageEvent.Attachments + Attachment{URL,ContentType,FileType}.
- pipeline/repository: AppendAttachments/GetAttachments on a parallel Redis list
  (bot_runtime:attach:{contact}:{conv}), aggregated like the text buffer and cleared
  together in ClearState (no stale media leaks into the next turn).
- debounce/service: Start/Reset accept attachments; GetAttachments added.
- pipeline/service: thread event.Attachments through start/skip/reset/advance -> the
  A2ARequest (read fresh from Redis at stage launch, like the buffer).
- ai/model: A2ARequest.Attachments; JSONRPCPart.File + JSONRPCFile{Name,MimeType,Bytes}
  (tags match the processor's extract_files_from_message).
- ai/service/ai_adapter: download each attachment once (before Marshal, reused across
  retries) with a 15 MiB cap; base64-encode; append a file part. A download failure is
  logged and skipped so the text-only message always survives.
- tests: adapter forwards a file part with decodable base64 + download-failure sends
  text only; repo AppendAttachments/GetAttachments roundtrip + ClearState clears the
  attach key. Full suite green (go build/vet/test ./pkg/... ./internal/...).

Note: test/e2e was already incompatible with NewAIAdapter on develop (pre-existing);
repo CI is docker-only.
Review follow-ups on the incoming-media path. The per-file cap was the only
bound, so the failure modes it did not cover fell back on the customer losing
the whole reply instead of just the media.

- Shared byte budget (20 MiB) across every attachment of the call. The debounce
  window aggregates the media of all its messages, so a photo burst built a body
  of len(attachments) x 15 MiB; base64 pushed that past the gateway's
  client_max_body_size and the resulting 413 is not retryable, killing the text
  reply too. Probe: 20 x 2 MiB went from a 53 MiB request to 26 MiB.
- Dedicated download timeouts. Downloads run before the AI call and outside its
  retry ceiling, but reused AI_CALL_TIMEOUT_SECONDS (30s) per attachment, so an
  unreachable media host stalled the turn by 30s x len(attachments) with no
  bound. Now 10s per download and 30s for the whole set.
- Resolve the mime type from the bytes in hand: the response Content-Type wins,
  then the CRM's declared type, then the URL extension. The processor feeds this
  straight into Blob(mime_type=...), so an HTML error/login page answered with
  200 was being forwarded as a valid image, and a missing content_type became
  application/octet-stream. Both are now dropped or resolved.
- A Redis failure on the attachment buffer no longer aborts the turn: media is
  best-effort everywhere else in this path, and dropping the text reply over it
  contradicted the card's own acceptance criterion.

Tests: the event -> debounce -> Redis -> A2ARequest seam had no coverage (the
debounce mock always returned nil attachments), so a refactor could silently
drop the media; two pipeline tests now pin it, including aggregation across the
debounce window. Adapter tests cover the byte budget, the time budget, HTML
responses, oversize files and the mime resolution table.

Also repairs test/e2e, which has not compiled since EVO-2167 changed
NewAIAdapter/NewDispatchEngine — which is why `go vet ./...` and `go test ./...`
could not be run at all. Two assertions had drifted: the message signature moved
to a prefix on the first segment in EVO-558, and the state-leak check raced the
cleanup goroutine it was asserting on.

go build ./... && go vet ./... && go test ./... green, e2e included.
…inbound-media

feat(EVO-2180): forward incoming media to the AI Processor as A2A file parts
Review follow-up to EVO-2180. Attachment URLs arrive inside the /events
payload and the adapter fetched them verbatim, so the endpoint doubled as a
read primitive aimed by its caller: the bytes of any URL reachable from this
service were base64-encoded into the A2A call, whose destination
(outgoing_url) comes from the same payload. Reproduced end to end against a
local metadata-style endpoint, both directly and through a 302.

- checkMediaURL pins the scheme to http/https and requires the host to be one
  the CRM is known to serve blobs from: the postback URL's host (already
  mandatory in MessageEvent.Validate, so no new config for the default
  topology) plus whatever MEDIA_HOST_ALLOWLIST names, for deployments serving
  blobs off an S3/MinIO/CDN host. Unauthorized media is skipped and logged;
  the text reply is unaffected, like every other media failure here.
- The download client re-runs that check on every redirect hop, so an
  authorized host cannot walk the fetch onto an internal address.
- BOT_RUNTIME_SECRET becomes required. It was read with os.Getenv, and
  SecretMiddleware compares the header against it, so an empty value
  authenticated every caller that simply omitted the header.
- The media buffer key gets a TTL. ClearState remains the normal cleanup; the
  TTL only stops a turn that dies before reaching it from leaving media URLs
  in Redis forever.
- Attachment download failures now log the HTTP status: the common production
  case is a 404 from a signed link that expired while the queue was backed up,
  and it read identically to an unreachable host.
- Adds .github/workflows/ci.yml. Nothing ran the Go suite on a PR, which is
  how test/e2e stayed non-compiling from EVO-558 until EVO-2180.
…srf-guard

fix(EVO-2178): validate incoming media URLs before fetching them
…e event

Fixes a regression I introduced in #6. The allowlist was anchored on the host
of the event's postback_url, which never matches the host the CRM actually
signs media URLs with: postback_url comes from BOT_RUNTIME_POSTBACK_BASE_URL
(internal DNS, "evo-crm" in the shipped compose) while the URL is built from
ACTIVE_STORAGE_URL, falling back to BACKEND_URL — which production requires to
be a public host. So on develop every attachment was rejected as
blocked_url and the agent stopped seeing images: the EVO-2178 bug, back.

Anchoring on the event was also the wrong shape for the guard. Whoever sends
the event chooses every field in it, including the one being used to decide
what that same event may reach, so the check constrained nobody it needed to.
Reading MEDIA_HOST_ALLOWLIST only puts the decision with the operator, where
it cannot be chosen by the caller. A2ARequest.PostbackURL is dropped again.

The scheme check and the per-redirect re-check are unchanged.

This makes the variable required wherever media is expected: unset means no
attachment is fetched. The deploy surfaces are wired up in the umbrella PR;
k8s/configmap.yaml and k8s/deployment.yaml carry it here.
…lowlist-from-config

fix(EVO-2178): take the media host allowlist from config, not from the event
…provider-feedback

fix(pipeline): tell the customer when the AI backend fails (CRM-236)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @gomessguii, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 15 hours and 3 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

This release promotes develop to main while introducing CI, stricter runtime configuration, resilient AI Processor retries and failure notices, and complete best-effort media forwarding through the debounced pipeline with SSRF, size, MIME, and time safeguards.

Sequence diagram for debounced media forwarding and AI retry

sequenceDiagram
    participant CRM as CRM
    participant Pipeline as PipelineService
    participant Redis as Redis
    participant Adapter as AIAdapter
    participant Media as MediaHost
    participant AI as AIProcessor
    participant Dispatch as DispatchEngine

    CRM->>Pipeline: Process(MessageEvent)
    Pipeline->>Redis: AppendAttachments
    Pipeline->>Redis: GetAttachments
    Pipeline->>Adapter: Call(A2ARequest with Attachments)
    loop Each authorized attachment
        Adapter->>Media: HTTP GET attachment URL
        Media-->>Adapter: Media bytes and Content-Type
    end
    loop Transient failure and retries remain
        Adapter->>AI: HTTP POST JSON-RPC with text and file parts
        AI-->>Adapter: 429, 5xx, or network error
        Adapter->>Adapter: backoffDelay
    end
    Adapter->>AI: HTTP POST JSON-RPC
    AI-->>Adapter: Successful response
    Adapter-->>Pipeline: NormalizedResponse
    Pipeline->>Dispatch: Dispatch
    Dispatch-->>CRM: Agent reply
Loading

Flow diagram for AI failure notification

flowchart TD
    A[AIAdapter.Call] --> B{AI call succeeds?}
    B -->|Yes| C[Dispatch]
    B -->|No| D[clearStateWithLog]
    D --> E{AI_FAILURE_NOTICE configured?}
    E -->|Disabled| F[End]
    E -->|Enabled or default| G[sendAIFailureNotice]
    G --> H{postback URL available?}
    H -->|No| F
    H -->|Yes| I[Dispatch]
    I --> F
Loading

File-Level Changes

Change Details Files
Adds CI coverage and tightens production configuration defaults and required secrets.
  • Runs Go build, vet, and tests on pull requests and protected branches with Redis.
  • Requires BOT_RUNTIME_SECRET and introduces configurable AI timeout, retry count, and retry backoff.
  • Sets the production AI timeout to 90 seconds and wires the media host allowlist into Kubernetes.
.github/workflows/ci.yml
internal/config/config.go
k8s/configmap.yaml
k8s/deployment.yaml
cmd/server/main.go
Implements resilient AI Processor calls with bounded retries and customer-visible failure handling.
  • Retries transient network, 429, and 5xx failures with exponential jittered backoff while preserving a single request body.
  • Adds per-attempt timeouts, an overall retry ceiling, and non-retry behavior for cancellation, timeouts, 4xx, and decode failures.
  • Dispatches a configurable, provider-error-free notice when AI processing fails without overwriting follow-up pipeline state.
pkg/ai/service/ai_adapter.go
pkg/ai/service/ai_adapter_retry_test.go
pkg/pipeline/service/pipeline_service.go
pkg/pipeline/service/ai_failure_notice_test.go
test/e2e/e2e_test.go
Adds end-to-end ingestion, buffering, validation, and forwarding of incoming media as A2A file parts.
  • Carries attachments through events, debounce state, Redis storage, pipeline stages, and AI requests.
  • Downloads allowlisted HTTP(S) media once, enforces per-file, aggregate-size, and shared-time budgets, validates MIME types, and skips failures without dropping text.
  • Encodes accepted media as base64 JSON-RPC file parts and guards redirects and unauthorized hosts against SSRF.
pkg/pipeline/model/pipeline.go
pkg/pipeline/repository/pipeline_repository.go
pkg/pipeline/repository/redis_pipeline_repository.go
pkg/debounce/service/debounce_engine.go
pkg/pipeline/service/pipeline_service.go
pkg/ai/model/a2a.go
pkg/ai/service/ai_adapter.go
pkg/ai/service/ai_adapter_media_test.go
pkg/ai/service/ai_adapter_ssrf_test.go
pkg/pipeline/repository/redis_pipeline_repository_test.go
pkg/pipeline/service/pipeline_service_test.go
Updates adapters, mocks, harnesses, and regression tests for the new APIs and release behavior.
  • Adjusts constructors and interfaces for retry settings, dispatch secrets, and attachment-aware debounce operations.
  • Adds coverage for attachment persistence, forwarding, MIME fallback, limits, SSRF defenses, retries, failure notices, and pipeline isolation.
  • Updates segmentation expectations and test harness configuration for outgoing AI URLs and authenticated dispatch.
pkg/ai/service/ai_adapter_test.go
pkg/debounce/service/debounce_engine_test.go
pkg/pipeline/handler/handler_test.go
pkg/pipeline/service/pipeline_service_test.go
pkg/pipeline/repository/redis_pipeline_repository_test.go
test/e2e/e2e_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gomessguii
gomessguii merged commit 1c11624 into main Aug 28, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants