Skip to content

feat: send submission and budget events to quilt - #4

Merged
Adambomb210 merged 7 commits into
claude/exciting-sagan-7oh2zhfrom
jaspermayone/3-quilt-events
Oct 11, 2026
Merged

Adambomb210 merged 7 commits into
claude/exciting-sagan-7oh2zhfrom
jaspermayone/3-quilt-events

Conversation

@jaspermayone

Copy link
Copy Markdown
Member

Why

Quilt now has a patch API (patchworklabsorg/quilt#8). Krater is the app of the Ganymede patch. Quilt must know about each submission and about the budget that Krater approves, releases and spends. Weave adds the quilt scope in patchworklabsorg/weave#176.

What changed

  • Outbox. New table quilt_outbox. The row id is the event id. A service writes the rows in the same transaction as the change, so a rollback removes both.
  • Mapping. krater.services.quilt_events.sync_project compares a project with its outbox rows and adds only the difference. The project services and the SkyPilot spend reconciler call it. The budget events follow BudgetEntry rows one to one.
  • Token. WeaveClient.quilt_token() gets a client_credentials token with the quilt scope. The live client caches tokens by scope, with the existing cache code. The stub returns a fixed fake token.
  • Sender. krater.quilt holds the HTTP client and the sender. It is the only code that knows Quilt's URLs. The worker runs quilt_deliver every minute. A web action that commits also defers quilt_deliver_now (with a queueing lock).
  • Delivery rules. Oldest first. Order is kept for each submission: a row that fails holds back the later rows of its submission. Rows are taken with SELECT ... FOR UPDATE SKIP LOCKED. Backoff starts at 30 s and doubles up to 1 hour.
  • Admin. /admin has a "Delivery to Quilt" section. It shows waiting rows and rows that Quilt refused, with Retry and Dismiss. Both need a reason and write an AuditEvent.
  • Backfill. uv run python scripts/quilt_backfill.py adds events for existing projects. Event ids are uuid5 values from the source rows, so a second run adds nothing.
  • Settings. KRATER_QUILT_URL (blank = do not send yet), KRATER_QUILT_TIMEOUT_SECONDS, KRATER_QUILT_BATCH_SIZE. In production the URL must be blank or https://.
  • Docs. New docs/quilt-integration.md. CLAUDE.md and docs/SPEC.md link to it. The Weave e2e provisioning script gives the e2e Krater app the quilt scope.

Sequence

sequenceDiagram
  participant S as Krater service
  participant DB as Postgres (quilt_outbox)
  participant Wk as Krater worker
  participant W as Weave
  participant Q as Quilt
  S->>DB: domain change + outbox rows (one transaction)
  Wk->>DB: SELECT next due row FOR UPDATE SKIP LOCKED
  Wk->>W: POST /oauth/token (client_credentials, scope=quilt), cached
  W-->>Wk: access_token
  Wk->>Q: POST /api/v1/events (Bearer access_token)
  Q-->>Wk: 201 / 200 / 409 / 422 / 503 ...
  Wk->>DB: sent, retry later, or failed; commit
Loading

Mapping

external_id is the Krater Project.id. status is Krater's project status.

Krater change Event Data
First submission of a proposal submission.created applicant_sub, title, status, requested_cents, url ({KRATER_BASE_URL}/projects/{id}), submitted_at
Change of status, title, requested amount or link submission.updated Only the changed fields
BudgetEntry > 0 (initial approval, amendment up, admin adjustment up) budget.committed amount_cents, actor_sub
BudgetEntry < 0 (amendment down, admin adjustment down, reclaim on withdrawal, completion or by an admin) budget.released amount_cents capped at Quilt's remaining commitment, actor_sub
BudgetEntry = 0 nothing Row stored as skipped
New SpendSnapshot with a different total spend.recorded spent_cents_total
Quilt answers Result
201, 200 sent
409 unknown_submission, 5xx, no answer retry with backoff
401, 403, other unexpected status retry with backoff, the run stops, a configuration error is logged (401 also drops the cached token)
409 id_conflict / submission_exists, 413, 422 failed, shown on /admin

Setup

  1. Deploy feat: let resource servers introspect client_credentials tokens weave#176 and patchworklabsorg/quilt#8.
  2. In Weave, allow the quilt scope on the Krater app.
  3. In Weave, a superadmin allows the introspect scope on the Quilt app.
  4. In Quilt, set the Ganymede patch's Weave client id to Krater's KRATER_WEAVE_CLIENT_ID.
  5. Set KRATER_QUILT_URL on the portal and the worker.
  6. Run uv run python scripts/quilt_backfill.py once.

While KRATER_QUILT_URL is blank, the services still write the outbox rows. The sender does nothing and logs once. When the URL is set, the worker sends the backlog in order.

Tests

710 tests pass (88 new). They cover each mapping, the same-transaction rollback, the release cap, idempotent sync and backfill, each response class, the order for each submission, SKIP LOCKED with two real connections, the token scope and cache, the worker wiring, the production setting check, and the admin page with authz and CSRF. ruff check, ruff format --check and alembic check pass. The migration has one head.

Closes #3

@jaspermayone jaspermayone added the migration Includes a database migration label Oct 10, 2026
@Adambomb210
Adambomb210 merged commit 38a0fa0 into claude/exciting-sagan-7oh2zh Oct 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

migration Includes a database migration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants