CopilotKit / CopilotKit/outpost
Inbound idempotency: redelivered events create duplicate tickets, so a conversation can get more than one AI answer
Nobody has claimed this yet.
- 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:158—handleNewTicketdoes no
find-first on(source, sourceId), and there is no unique constraint backing it
(schema.prismahas 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— noMessageIDidempotency.
Postmark retries on any non-2xx, and the handler returns 500 ifcreateJobthrows
afterticket.createsucceeds, so the retry duplicates both.apps/github-app/src/webhooks/issue-comment.ts:53— nocomment.ididempotency;
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 duplicateThreadCreate
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
- Migration: unique on
Ticket(source, sourceId)wheresourceIdis non-null, plus
whatever partial-unique expresses "at most one AI-generated BOT message per ticket". - 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. - Have
handleNewTicketupsert-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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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