feat(agentex): support SYNC agents in the Slack gateway - #392
Merged
Conversation
The gateway hard-coded task/create + event/send, which only async/agentic agents accept — routing a SYNC agent failed with "Unsupported method: EVENT_SEND for ACP type: SYNC". Branch _dispatch on agent.acp_type: SYNC agents get a single message/send (get-or-create by the thread task name + the reply returned synchronously); async/agentic keep task/create (first turn) + event/send + poll. Reply text is extracted the same way for both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eway
The SYNC path's single message/send get-or-creates the thread task, so two
concurrent first messages for the same thread race on the globally-unique
task name — the loser raised DuplicateItemError straight up to _run_turn
("Something went wrong"), unlike the async path which already falls back.
Retry the message/send on DuplicateItemError: get-or-create then finds the
winner's task and appends the turn's message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lookups that resolve the winning task after a create-race read the read-only replica (get_task -> read-only session), while the create hits the primary. So a single retry can still miss the just-created task under replica lag and race the create again — the SYNC path's retry then raised DuplicateItemError uncaught, and the async fallback's get_task raised ItemDoesNotExist uncaught; both dropped the turn. Retry with a bounded loop + short backoff so replication catches up: - SYNC: _message_send_with_race_retry loops message/send on DuplicateItemError. - ASYNC: _resolve_task_after_race loops get_task on ItemDoesNotExist. Both give up after SLACK_CREATE_RACE_ATTEMPTS (persistent lag -> surfaced), and each raising attempt fails before any write, so retries never duplicate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
michael-chou359
added a commit
that referenced
this pull request
Aug 10, 2026
## What A platform-side ingress that fronts a Linear **agent app** and routes each @mention / assignment to the resolved agent runtime — the Linear analog of the Slack gateway (#388 / #392 / #394). ## How it works - **`POST /linear/events`** — verifies the `Linear-Signature` (HMAC-SHA256 over the raw body) + a `webhookTimestamp` freshness guard, dedups on the `Linear-Delivery` id, acks fast (Linear's ~10s window), and runs the turn in the background. Auth-whitelisted like `/slack` — Linear can't present an SGP principal, so the signature is the auth, verified in the use case. - **Normalize `AgentSessionEvent`** (`created` / `prompted`) → the same selector-cascade + `task/create`-or-`event/send` dispatch as the Slack gateway, keyed on the agent session (`task_metadata.channel = "linear"`). - **Reply via `agentActivityCreate`** — a `thought` immediately (a session is marked unresponsive without an activity within ~10s), then a terminal `response` / `error`. The Linear API token is minted via the OAuth **`client_credentials`** grant (inherently app-actor) and re-minted reactively on a 401 — no perishable token is stored, only the static client id/secret in env. - Removes a duplicate `slack.router` registration in `app.py`. ## Identity Runs as a **dedicated bot service account** (its own SGP identity), not a proxy for the invoking Linear user — consistent with the Slack gateway's identity model. Its comments/activities render as the app (`actor=app`). ## Config (env / k8s-secret) `LINEAR_CLIENT_ID`, `LINEAR_CLIENT_SECRET`, `LINEAR_WEBHOOK_SIGNING_SECRET`, `LINEAR_GATEWAY_ACTING_BOT_API_KEY`, `LINEAR_GATEWAY_ACCOUNT_ID`. ## Testing - 20 unit tests: signature verify, normalize (created / prompted / ignored), event control-flow (dev-skip / drop / dedup / ack), acting-identity fail-closed, dispatch metadata, and `agentActivityCreate` token-mint + 401 re-mint. All pass, ruff clean. - Signed ingress round-trip verified over HTTP; `client_credentials` token mint verified against the live Linear API (app-actor confirmed via `viewer`). - The full agent round-trip (posting an activity to a **real** session) validates post-deploy with a real @mention — a fabricated session can't be tested locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- greptile_comment --> <h3>Greptile Summary</h3> The PR adds a signed Linear webhook gateway that normalizes agent-session events, dispatches them through AgentEx under a bot identity, and posts activities back through Linear’s API. - Registers and documents `POST /linear/events`. - Adds signature freshness checks, delivery deduplication, target resolution, task dispatch, and background response collection. - Adds OAuth token minting and Linear activity delivery with one refresh attempt after a 401. - Adds unit coverage for ingress, normalization, identity handling, dispatch, and activity delivery. <details><summary><h3>Confidence Score: 3/5</h3></summary> The PR is not yet safe to merge because concurrent turns can receive each other’s replies and Linear activity-delivery failures can silently discard terminal results. Concurrent turns for one session share an uncorrelated message stream, while failed token minting or activity posting returns normally without delivering a response or error; both previously reported failures remain in the current code. **Files Needing Attention:** agentex/src/domain/use_cases/linear_gateway_use_case.py </details> <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | agentex/src/domain/use_cases/linear_gateway_use_case.py | Implements the Linear gateway end to end; previously reported reply-correlation and silent activity-delivery failures remain. | | agentex/src/api/routes/linear.py | Adds the thin `/linear/events` ingress that preserves the raw body for signature verification. | | agentex/src/api/middleware_utils.py | Whitelists the Linear route so webhook authentication can be enforced by signature verification in the use case. | | agentex/src/api/app.py | Registers the new Linear router while removing the duplicate Slack-router registration. | | agentex/openapi.yaml | Documents the new Linear webhook endpoint and its successful response. | | agentex/tests/unit/use_cases/test_linear_gateway_use_case.py | Covers core gateway behavior but does not eliminate the blocking failures that remain in concurrent reply attribution and activity delivery. | </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant L as Linear participant R as POST /linear/events participant G as LinearGatewayUseCase participant A as AgentEx ACP participant API as Linear API L->>R: Signed AgentSessionEvent R->>G: Raw body, headers, payload G->>G: Verify signature and deduplicate G-->>L: 200 acknowledgement G->>API: Create thought activity G->>A: Resolve target and send event A-->>G: Task messages G->>API: Create response or error activity ``` </details> <sub>Reviews (2): Last reviewed commit: ["fix(agentex): address Linear gateway rev..."](88283a5) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=51719191)</sub> <!-- /greptile_comment --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Let the Slack gateway invoke
ACPType.SYNCagents, not just async/agentic ones.Why
_dispatchhard-codedtask/create+event/send.event/sendis only in the allowed method set forASYNC/AGENTICagents, so routing@agent <a-sync-agent>from Slack failed the turn with:and the user just saw "Something went wrong."
How
Branch
_dispatchonagent.acp_type:message/send(SendMessageRequestEntity), which get-or-creates the thread task and returns the reply messages synchronously — notask/create, noevent/send, no polling.task/createon the first turn (with theDuplicateItemErrorcreate-race fallback) +event/send+ poll for the settled reply.Reply text is extracted the same way for both (
_agent_text), and first-turn params (config_id / default MCPs) are built once and passed to whichever create path runs.task_metadata.channel="slack"(origin gating for golden-agent) is still set on the async path only, which is where it applies.Testing
message/send(task_name = the thread key) and the reply is returned directly; the async/agentic tests are unchanged (the fake agent now carriesacp_type).🤖 Generated with Claude Code
Greptile Summary
The PR adds Slack gateway support for synchronous ACP agents and introduces bounded recovery for concurrent task-creation races.
message/sendrequest.task/create,event/send, and reply polling for ASYNC and AGENTIC agents.Confidence Score: 4/5
The PR is not yet safe to merge because concurrent first turns can still be dropped when read-replica lag outlasts the fixed retry window.
Both previously reported create-race paths now retry, but they still depend on the same read-only lookup becoming consistent within a default 0.75-second backoff period; after that period, the SYNC path propagates
DuplicateItemErrorand the async path propagatesItemDoesNotExist, producing the generic Slack failure response.Files Needing Attention: agentex/src/domain/use_cases/slack_gateway_use_case.py
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Slack turn] --> B{Agent ACP type} B -->|SYNC| C[message/send] C --> D{DuplicateItemError?} D -->|No| E[Return synchronous reply] D -->|Yes| F[Backoff and retry] F --> C B -->|ASYNC or AGENTIC| G[Look up thread task] G --> H{Task exists?} H -->|No| I[task/create] I --> J{DuplicateItemError?} J -->|Yes| K[Retry read-only task lookup] J -->|No| L[event/send] K -->|Task visible| L K -->|Retries exhausted| M[Generic Slack failure] H -->|Yes| L L --> N[Poll and return reply]Comments Outside Diff (1)
agentex/src/domain/use_cases/slack_gateway_use_case.py, line 497 (link)If the configured read replica remains behind the primary longer than the fixed retry window, every
get_taskattempt misses the winning task and the helper raisesItemDoesNotExist, causing_run_turnto drop the Slack turn and return the generic failure response.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "fix(agentex): make the Slack create-race..." | Re-trigger Greptile