Skip to content

feat(agentex): support SYNC agents in the Slack gateway - #392

Merged
michael-chou359 merged 3 commits into
mainfrom
mc/slack-gateway-sync-agents
Aug 4, 2026
Merged

feat(agentex): support SYNC agents in the Slack gateway#392
michael-chou359 merged 3 commits into
mainfrom
mc/slack-gateway-sync-agents

Conversation

@michael-chou359

@michael-chou359 michael-chou359 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What

Let the Slack gateway invoke ACPType.SYNC agents, not just async/agentic ones.

Why

_dispatch hard-coded task/create + event/send. event/send is only in the allowed method set for ASYNC/AGENTIC agents, so routing @agent <a-sync-agent> from Slack failed the turn with:

ClientError: Unsupported method: AgentRPCMethod.EVENT_SEND for ACP type: ACPType.SYNC

and the user just saw "Something went wrong."

How

Branch _dispatch on agent.acp_type:

  • SYNC → a single message/send (SendMessageRequestEntity), which get-or-creates the thread task and returns the reply messages synchronously — no task/create, no event/send, no polling.
  • ASYNC / AGENTIC (e.g. golden-agent) → unchanged: task/create on the first turn (with the DuplicateItemError create-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

  • New unit test: a SYNC agent triggers exactly one message/send (task_name = the thread key) and the reply is returned directly; the async/agentic tests are unchanged (the fake agent now carries acp_type).
  • Full gateway unit suite green (52), lint clean.

🤖 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.

  • Routes SYNC agents through a single synchronous message/send request.
  • Preserves task/create, event/send, and reply polling for ASYNC and AGENTIC agents.
  • Adds retry helpers and tests for create races and temporary replica lag.

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 DuplicateItemError and the async path propagates ItemDoesNotExist, producing the generic Slack failure response.

Files Needing Attention: agentex/src/domain/use_cases/slack_gateway_use_case.py

Important Files Changed

Filename Overview
agentex/src/domain/use_cases/slack_gateway_use_case.py Adds ACP-type-specific dispatch and race retries, but bounded read-replica recovery still drops turns when replication lag exceeds the configured window.
agentex/tests/unit/use_cases/test_slack_gateway_use_case.py Covers SYNC dispatch and short-lived replica lag, but does not establish safe recovery when lag outlasts the retry window.

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]
Loading

Comments Outside Diff (1)

  1. agentex/src/domain/use_cases/slack_gateway_use_case.py, line 497 (link)

    P1 Replica retry still drops turns

    If the configured read replica remains behind the primary longer than the fixed retry window, every get_task attempt misses the winning task and the helper raises ItemDoesNotExist, causing _run_turn to drop the Slack turn and return the generic failure response.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: agentex/src/domain/use_cases/slack_gateway_use_case.py
    Line: 497
    
    Comment:
    **Replica retry still drops turns**
    
    If the configured read replica remains behind the primary longer than the fixed retry window, every `get_task` attempt misses the winning task and the helper raises `ItemDoesNotExist`, causing `_run_turn` to drop the Slack turn and return the generic failure response.
    
    ---
    
    For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

    Fix in Cursor Fix in Claude Code Fix in Codex

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
agentex/src/domain/use_cases/slack_gateway_use_case.py:497
**Replica retry still drops turns**

If the configured read replica remains behind the primary longer than the fixed retry window, every `get_task` attempt misses the winning task and the helper raises `ItemDoesNotExist`, causing `_run_turn` to drop the Slack turn and return the generic failure response.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (3): Last reviewed commit: "fix(agentex): make the Slack create-race..." | Re-trigger Greptile

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>
@michael-chou359
michael-chou359 requested a review from a team as a code owner August 4, 2026 17:43
Comment thread agentex/src/domain/use_cases/slack_gateway_use_case.py Outdated
…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>
Comment thread agentex/src/domain/use_cases/slack_gateway_use_case.py Outdated
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
michael-chou359 merged commit 28a1a00 into main Aug 4, 2026
46 checks passed
@michael-chou359
michael-chou359 deleted the mc/slack-gateway-sync-agents branch August 4, 2026 20:44
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant