Skip to content

feat(arbiter): event-id dedupe for supervisor and fabricator - #103

Merged
gibbsie merged 1 commit into
mainfrom
feat/agent-event-idempotency
Sep 1, 2026
Merged

feat(arbiter): event-id dedupe for supervisor and fabricator#103
gibbsie merged 1 commit into
mainfrom
feat/agent-event-idempotency

Conversation

@gibbsie

@gibbsie gibbsie commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

The DLQ audit classified the supervisor and fabricator consumers as redrive-unsafe: EventBridge delivers at-least-once, and a duplicate (or a DLQ redrive) re-ran orchestration and re-fabricated agents - the fabricator's duplicate-fabrication history is documented. This PR makes both consumers idempotent so the new shared DLQs can actually be redriven.

  • Supervisor: a single conditional-put claim on the literal event.id in the existing 'citadel-idempotency-‹env› table (referenced by name, no new table); duplicate task.request /task.completion events become no-ops; claim rows carry a 7-day TTL
  • Fabricator: two-phase PENDING-DONE claim keyed on the SQS messageId - its queue has no EventBridge envelope (both producers call SendMessage directly), so messageld is the stable identity across redeliveries and redrives
  • Fail-closed: on dedupe-store errors the handler raises with no side effects, SO the message redelivers and eventually dead-letters - never a silent skip
  • CDK: both Lambdas gain IDEMPOTENCY_TABLE env + read/write grants on the shared table; zero new DynamoDB tables synthesized

Testing

  • 18 new pytest cases (red-first) plus 4 CDK assertions; scoped suites all exit 0 (supervisor 183, fabricator 360, workerwrapper 379, common 268)
  • Full arbiter suite delta vs main: +18 tests, 0 new failures; tse clean; all ArbiterStack jest suites green
  • Independent moto proofs against real handlers + real DynamoDB engine: on main, a duplicate event. id runs orchestration twice and a redrive re-fabricates a DONE message; on this branch, side effects fire exactly once (supervisor 1/7, fabricator 8/8), including the poisoned-then-fixed redrive and operator row-clean paths

Notes

  • Poison fabricator messages ack on second receive (ALREADY_PENDING) and never reach 'citadel-fabricator-dlq' - triage anchors on stale PENDING rows; the redrive runbook PR documents this, and a stale-PENDING metric/alarm is tracked as a follow-up
  • Branch was cut from pre-merge main (8685b93); both this and PR 102 touch arbiter-stack.ts, so expect a trivial rebase/merge if the hunks collide

…nsumers

CIT-125 slice B: at-least-once EventBridge delivery + SQS DLQ redrive safety.

Supervisor (EventBridge-fed): single conditional-put claim on event['id']
in the shared citadel-idempotency-<env> table (PK eventId, TTL). Duplicate
task.request/task.completion deliveries are a no-op; non-conditional-check
DDB errors rethrow so the message redelivers/DLQs.

Fabricator (SQS-fed, no EventBridge envelope): two-phase PENDING/DONE claim
keyed on the SQS messageId instead of event.id (deviation, documented in
module + test docstrings and reply) — the fabricator queue has no EB rule
target; both producers (fabricator-request-resolver.ts, agent-import-
resolver.ts) SendMessage directly with no id/detail field. messageId gives
the same at-least-once/redrive-stability guarantee the design relies on.
A redelivery seen while PENDING routes to reconcile instead of
re-fabricating; a redelivery of a DONE message is a no-op.

Both reuse the existing table by name (Table.fromTableName in
arbiter-stack.ts) with RW grants + IDEMPOTENCY_TABLE env — no new table,
no new CFN edge, no split-gates-gated resource touched (arbiter isn't
synthesized by split-gates).

Red-first tests: 18 new pytest cases (9 supervisor, 9 fabricator) + 4 CDK
wiring tests (table-count-unchanged, both envs, both RW grants).
@gibbsie
gibbsie merged commit 777b5e2 into main Sep 1, 2026
21 of 22 checks passed
@gibbsie
gibbsie deleted the feat/agent-event-idempotency branch September 1, 2026 19:32
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