CopilotKit / CopilotKit/outpost

Inbound idempotency: redelivered events create duplicate tickets, so a conversation can get more than one AI answer

Open
#169 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: ai roadmap roadmap: now
Dominant language
TypeScript
Stars
7
Forks
3
Avg merge
7d 16h
Merged PRs (30d)
15

Description

Outpost is meant to answer exactly one message per ticket. That invariant is enforced
per ticket (see the guard in packages/outpost/queue/src/handlers/ai-response.ts), so
it holds only as long as one conversation maps to one ticket. None of the four inbound
doors guarantee that: every one of them will happily create a second ticket for a
message it has already seen, and the second ticket gets its own AI answer. From the
reporter's side that is indistinguishable from the bot answering twice.

Found by the review round on the one-response-per-ticket change; deferred because the
fix needs a schema migration and touches all four doors, which is a different blast
radius than that change had.

Sites
  • packages/outpost/shared/src/platforms/inbound.ts:158handleNewTicket does no
    find-first on (source, sourceId), and there is no unique constraint backing it
    (schema.prisma has an index only). A redelivered thread-start creates a second
    ticket and a second answer.
  • apps/web/src/app/api/webhooks/postmark/route.ts:102 — no MessageID idempotency.
    Postmark retries on any non-2xx, and the handler returns 500 if createJob throws
    after ticket.create succeeds, so the retry duplicates both.
  • apps/github-app/src/webhooks/issue-comment.ts:53 — no comment.id idempotency;
    redelivery duplicates messages.
  • apps/slack-bot/src/events/message.ts:41 — no thread-start dedupe.
  • apps/discord-bot/src/events/thread-create.ts:75 — a duplicate ThreadCreate
    duplicates ticket and answer.
Also fixed by the same constraint

packages/outpost/queue/src/handlers/ai-response.ts:97 — the one-response guard is
check-then-write. AI_RESPONSE runs at concurrency 4 (apps/worker/src/index.ts:63),
so two jobs for the same ticket can both read "no BOT row yet" and both answer. A
transaction around the check does not close this across connections; a uniqueness
constraint does. That is why it belongs here rather than in the guard's own PR.

Suggested shape
  1. Migration: unique on Ticket(source, sourceId) where sourceId is non-null, plus
    whatever partial-unique expresses "at most one AI-generated BOT message per ticket".
  2. Make each door idempotent on its natural platform key — MessageID, comment.id,
    Slack event id, Discord thread id — rather than re-deriving dedup per door.
  3. Have handleNewTicket upsert-or-return on (source, sourceId) so the shared handler
    is safe by construction and the bots inherit it.

Needs tests per door for the redelivery case; none exist today.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/outpost/shared/src/platforms/inbound.ts:158, the schema.prisma ticket constraints, and the four webhook or event entry points listed in the issue. Trace how each natural platform key reaches ticket creation, then inspect the AI response handler and worker concurrency. Done means the migration and shared handling prevent duplicate tickets or AI messages, with redelivery tests for each door and coverage for concurrent responses.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, typescript
Domain
api, backend, databases
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.