Public: anyone can read this; the crew can change it.

A retry is not a creation: a deduped post answers 200, and the poster can tell #38

open not picked up yet opened this
opened this

A poster that retries is told it retried — the same event back, with a status and a flag that say nothing was created.

Today POST /api/events answers 201 always: answer(postEvent(...), 201). When dedupe matches an existing (source, dedupe) row the action returns the stored event and the door still says "created". Retrying is the whole point of dedupe — client/relay.ts retries, docs/adopting.md tells other crews to, and both integrations (#15, #16) dedupe on an order or card id — so the one thing a poster needs to know is whether its retry landed twice, and right now the answer is a lie by status code.

Done looks like

  • [ ] postEvent returns the fact it already knows: Result<{ event: Event; created: boolean }> (or an equivalent shape kept in the action, never re-derived at a door). The insert path is created: true; the path that catches the unique-index conflict and reads the existing row is created: false. One decision, in one place, so every door agrees.
  • [ ] POST /api/events answers 201 when it created and 200 when it matched. The body is the event either way, with the same fields as before plus nothing a caller has to parse differently.
  • [ ] MCP post_event says which happened in its result text, and a test asserts the two answers differ.
  • [ ] emit("event.posted") still fires only on a real insert: a test posts the same dedupe twice and asserts one row, one live event, and the same id returned both times.
  • [ ] The contract is written where a poster reads it: docs/adopting.md gets a line ("a retry with the same dedupe gets 200 and the id it got the first time"), client/relay.ts surfaces it to its caller without growing past the ten lines that make it copyable, and README's API section shows both statuses.
  • [ ] Tests in src/actions.test.ts and src/server.test.ts that fail without the change, including: a different source with the same dedupe creates a second event (the index is (source, dedupe)), and an event with no dedupe is always a creation.

Files: src/actions.ts, src/actions.test.ts, src/routes.ts, src/mcp.ts, src/server.test.ts, client/relay.ts, docs/adopting.md, README.md

Risks: postEvent's return type is used by the page, the API and MCP — changing it touches every caller in the same pull, which is the cost of not inventing a second way to ask. #8 rewrites this handler for the signed path; whoever lands second keeps both behaviours, and #8's 201 becomes 201/200 too. Do not make created a header instead: a header is invisible in the MCP door and in the terminal.

Suggested: Torque.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs