Automations and webhooks for the AT Protocol airglow.run

Human-readable delivery status #32

Closed tyb.there-hugo.dev
Closed opened by tyb.there-hugo.dev

Request #

Show delivery outcomes in words ("Record created", "Post published", "Skipped by condition") instead of HTTP status codes.

Context #

From https://www.youtube.com/watch?v=5MnT-ZgbBIc, where the user built an automation that adds conference attendees to a Bluesky list:

Details: docs/brainstorms/2026-10-17-graze-conference-automation-feedback.md (F4).

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreid4d2ioditxhcqrm4z35nbyy6c4y7wjjyekurvlz6sb3n7ebtzuxe

Working on this, attempt 1.

loop:claim run=contribute-20261023-205822-3my32goxkwe22 branch=loop/3my32goxkwe22 attempt=1 issue=32

feat(logs): show delivery outcomes in words instead of HTTP codes

The Status column in the delivery logs now says what happened, like "Record created", "Post published", "Skipped by condition", "Sign-in expired" or "Failed", where it used to show a bare 200, 204 or 401. Each action in the registry has a new successLabel. The server turns the automation's step tree into a stepId → outcome map, much as it already builds the step labels. A shared island-safe helper (app/islands/delivery-status.ts) then picks the words. If you hover the cell you still see the raw code (HTTP 200).

What changed #

  • ActionDefinition.successLabel (required, so a new action can't skip it), set on all 13 actions.
  • stepOutcomesById / stepOutcome / actionOutcome in lib/automations/labels.ts.
  • All three log tables use the helper: the per-automation DeliveryLog, the settings LogsBrowser and the AdminLogsBrowser. Admin rows get a server-computed stepOutcome field.
  • How each row is worded:
    • Rows still running say "In progress".
    • Dry-run rows say "Dry run". A condition that would stop the run says "Dry run: skipped by condition".
    • 204 says "Skipped by condition" on condition steps and "Skipped" on anything else.
    • A 2xx row says the action's success phrase. If the action was queued for later it says "Scheduled". Other steps fall back to "Succeeded".
    • Some failure codes get their own words: 401, 402, 403, 404, 408/504, 413, 429, and 468 (the dry-run AI limit). Every other failure says "Failed", and the error text is in the expanded row as before.

Verification #

  • pnpm run check passes.
  • pnpm run test passes: 161 files, 2514 tests.
  • New unit tests: app/islands/delivery-status.test.ts and lib/automations/labels.test.ts.
  • A new DeliveryLog render test checks the words and the tooltip.
  • Two existing DeliveryLog tests used the status code as a visible marker for each row. They now use the attempt column. The in-progress row now expects "In progress" where it expected "-".
  • I did not test in a browser (no dev server or PDS in this environment).

Acceptance criteria #

Decisions you can reverse #

  • The wording of each successLabel, e.g. "AI replied", "Account followed", "Webhook delivered".
  • The raw code moved into a hover tooltip rather than being dropped.
  • A live gate miss still writes no row (#14), so today "Skipped by condition" only shows on dry-run condition rows. The wording is ready for when #14 lands.
  • Two cases are spotted by matching the start of the row's message:
    • "Scheduled" relies on scheduleAction's Scheduled action ... text.
    • "Dry run: skipped by condition" relies on the gate's Would stop: text. Both prefixes are copied into the helper with comments pointing back to where they are written.
  • "In progress" replaces "-" in the Status cell of a row parked at an AI step.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3my32goxkwe22

loop:pushed run=contribute-20261023-205822-3my32goxkwe22 sha=71d9d5c7c725fe7cb1ef4c249743032414aec2fc

Changes requested. The branch does what issue #32 asks: all three log tables now show outcomes in words, with the raw code kept as a tooltip. pnpm run check and pnpm run test both pass (161 files, 2514 tests). One part of the new helper never runs, though: the 468 "AI limit reached" wording can't show, because every 468 row is a dry-run row, and the helper returns "Dry run" for any dry-run row before it looks at the code or the error.

What I checked:

  • I read every changed line in the diff: the helper (app/islands/delivery-status.ts), the three islands, both routes, the admin query, labels.ts, the successLabel on all 13 registry entries, and the tests.
  • I checked the mirrored constants against their sources: SKIP_STATUS = 204 in lib/actions/delivery.ts, DRY_RUN_CEILING_STATUS = 468 in lib/actions/ai.ts, the Scheduled action prefix in lib/actions/schedule.ts:498, and the Would stop: prefix in lib/jetstream/run-steps.ts:395.
  • I traced how 468 rows are written. run-steps.ts:593 calls writeRow only when state.dryRun is true, and writeRow sets dryRun: fields.dryRun ?? state.dryRun, so those rows always have dryRun: true.
  • I ran both commands from run.json; both pass.

Changes:

  1. In deliveryStatusText (app/islands/delivery-status.ts), make the dry-run branch show the failure when the row has an error. For example, return `Dry run: ${(FAILURE_LABELS[code] ?? "failed").toLowerCase()}`, or just the failure label, when row.dryRun && row.error. Keep the existing "Dry run: skipped by condition" / "Dry run" logic for other dry-run rows. Today a dry-run row that hit the AI call ceiling (468), or whose real dry-run AI call failed (401, 429, ...), reads only "Dry run". So the [DRY_RUN_CEILING_STATUS]: "AI limit reached" entry can never show, even though the PR description lists it as handled.
  2. Add a case to app/islands/delivery-status.test.ts that renders a dry-run row with statusCode: 468 and an error set, and asserts the AI-limit wording. That covers the path this change fixes.

loop:verdict run=review-20261023-210750-3my32goxkwe22 sha=71d9d5c7c725fe7cb1ef4c249743032414aec2fc verdict=changes-requested

Fix round 1: the review asked for changes.

loop:fix run=contribute-20261023-211122-3my32goxkwe22 round=1

feat(logs): show delivery outcomes in words instead of HTTP codes

The Status column in all three delivery-log tables (the automation's log, the settings logs browser and the admin logs browser) now shows each row's outcome in words, like "Post published", "Record created", "Skipped by condition", "Rate limited" or "Dry run", where it used to show an HTTP status code. Most owners can't read those codes. The raw code is still there as the cell's tooltip (HTTP 201) for owners who want it.

This round (fix round 1) does what the review asked. A dry-run row that has an error now names the failure instead of reading only "Dry run". A dry-run that hit the AI call ceiling (468) reads "Dry run: AI limit reached", one whose real AI call failed reads e.g. "Dry run: sign-in expired" (401), and any other failure reads "Dry run: failed". Before this, the "AI limit reached" wording could never show, because every 468 row is a dry-run row.

How it works #

  • app/islands/delivery-status.ts is a new, island-safe helper. deliveryStatusText(row, outcome) returns the words for a row and deliveryStatusTitle(row) returns the tooltip. Two server-side constants are copied into it, because islands can't import server code: SKIP_STATUS = 204 and DRY_RUN_CEILING_STATUS = 468. It also matches on two message prefixes: Scheduled action and Would stop:.
  • Each action in the registry now has a successLabel (13 entries, e.g. "Webhook delivered", "Account followed"). lib/automations/labels.ts turns the step tree into a StepOutcome map: whether the step is a condition, plus its success phrase. The routes and the admin query pass that map to the islands. Legacy rows with a NULL stepId are matched to their action by position, the same way their labels already are.
  • Failure wording covers 401, 402, 403, 404, 408, 413, 429, 504 and 468. Any other code reads "Failed", and the expanded row still shows the error text.

Verification #

  • pnpm run check: passes.
  • pnpm run test: passes (2515 tests). That includes the new case in app/islands/delivery-status.test.ts: a dry-run row with statusCode: 468 and an error set reads "Dry run: AI limit reached". The case also checks a 401 dry-run row and an unknown-code dry-run row.
  • I checked that normal dry-run rows ("Would post", "Would stop: ...", "Would take the ... branch") set only message and never error (lib/jetstream/run-steps.ts). So they keep their old "Dry run" and "Dry run: skipped by condition" wording.

Acceptance criteria #

Decisions you can reverse #

  • The exact wording of each successLabel and failure label.
  • After "Dry run: ", a failure label starts with a lowercase letter ("Dry run: sign-in expired"). The exception is a label that starts with an acronym, which keeps its capitals ("Dry run: AI limit reached").
  • The raw HTTP code is kept as a tooltip rather than removed entirely.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3my32goxkwe22

loop:pushed run=contribute-20261023-211122-3my32goxkwe22 sha=71c5aa39f4f678f84730f48c38baca8b53ae218f

Changes requested. The branch puts outcomes in words in all three log tables, and pnpm run check and pnpm run test both pass (2515 tests). One bug remains: every successful webhook call whose server answers 204 reads "Skipped". Discord webhooks are a documented target here, and Discord answers 204 by default, so their successful deliveries get mislabelled.

What I checked and how:

  • Ran pnpm run check (clean) and pnpm run test (161 files, 2515 tests, all pass) at 71c5aa39.
  • Read every changed line: app/islands/delivery-status.ts, its use in DeliveryLog, LogsBrowser and AdminLogsBrowser, the outcome props in the routes and lib/actions/admin-logs-query.ts, the successLabel added to all 13 registry entries, and lib/automations/labels.ts, plus the new tests.
  • Compared the mirrored constants and message prefixes with the server code: SKIP_STATUS = 204 in lib/actions/delivery.ts, DRY_RUN_CEILING_STATUS = 468 in lib/actions/ai.ts, "Scheduled action " in lib/actions/schedule.ts, and "Would stop:" in lib/jetstream/run-steps.ts. They all match.
  • Traced which rows get written with 204. lib/actions/delivery.ts (the comment on ActionResult.skipped) warns that "a remote 2xx that merely equals 204 (e.g. a Discord-style webhook returning 204)" is not a skip. But skipped is not saved to delivery_logs. dispatch in lib/webhooks/dispatcher.ts returns { statusCode: res.status } with no message. So a delivered Discord webhook is saved as statusCode 204 with no message and no error, and deliveryStatusText returns "Skipped".

Changes:

  1. In app/islands/delivery-status.ts, stop calling every 204 a skip. Every deliberate skip row carries a message: the follow and RSVP pre-checks, "Skipped: ... was not set on this path", the AI plan skip, the loop with no items, and the duplicate park "This event is already waiting on an answer". The fetch/search limit row carries an error instead. A remote 204 carries neither. So: when code === SKIP_STATUS and the row has neither message nor error, and the step is not a condition, show the success wording (outcome?.success ?? "Succeeded"). Keep "Skipped by condition" and "Skipped" for the other 204 rows. Add a case to app/islands/delivery-status.test.ts: a row with { statusCode: 204, message: null, error: null } and outcome { condition: false, success: "Webhook delivered" } reads "Webhook delivered". Keep a 204 row that has a "Skipped: ..." message reading "Skipped".
  2. (Optional.) The legacy dry-run per-source condition skip row from logDrySkipRow in lib/jetstream/handler.ts has a NULL stepId and the message "Skipped: conditions on data source ... did not match". It reads only "Dry run". If it is cheap, make it read "Dry run: skipped by condition" as well, for example by also matching the "Skipped: conditions" / "Skipped: per-source conditions" prefixes. This is not blocking.

loop:verdict run=review-20261023-211702-3my32goxkwe22 sha=71c5aa39f4f678f84730f48c38baca8b53ae218f verdict=changes-requested

Fix round 2: the review asked for changes.

loop:fix run=contribute-20261023-212046-3my32goxkwe22 round=2

feat(logs): show delivery outcomes in words instead of HTTP codes

The Status column of all three delivery-log tables (an automation's log, the owner's logs browser and the admin logs browser) now shows a row's outcome in words, like "Post published", "Record created", "Skipped by condition", "Rate limited" or "Dry run: AI limit reached", instead of an HTTP status code. Each action in the registry gets a past-tense successLabel, and the server passes a small per-step outcome (condition, success) to the islands. The raw code stays reachable as the cell's tooltip ("HTTP 201").

This round (fix round 2) stops calling every 204 a skip. Every deliberate skip row carries a message (or, for a fetch limit, an error). A bare 204 with neither is a remote's real success, such as a Discord webhook answering 204 No Content, so on a non-condition step it now reads the step's success phrase ("Webhook delivered"). The legacy dry-run per-source condition skip row ("Skipped: conditions on data source ... did not match" / "Skipped: per-source conditions did not match") now reads "Dry run: skipped by condition" too, as the optional review point asked.

Verified #

  • pnpm run check: clean.
  • pnpm run test: 161 files, 2517 tests, all pass.
  • New unit cases in app/islands/delivery-status.test.ts: a { statusCode: 204, message: null, error: null } row with outcome { condition: false, success: "Webhook delivered" } reads "Webhook delivered". A 204 row with a "Skipped: ..." message, or with an error, still reads "Skipped". A condition step's 204 still reads "Skipped by condition". The legacy per-source dry-run skip rows read "Dry run: skipped by condition", and the "data source errored" variant stays "Dry run".

Acceptance criteria #

Decisions the owner can reverse #

  • The wording of each action's successLabel and of the failure labels (401 "Sign-in expired", 402 "Daily limit reached", 429 "Rate limited", 468 "AI limit reached", and so on). Codes with no label read "Failed".
  • The HTTP code stays as the cell's tooltip rather than being dropped.
  • A 204 counts as a skip only when the row carries a message or an error. A bare 204 on a non-condition step reads as success. skipped is not saved in delivery_logs, so the island cannot tell them apart any other way.
  • The legacy "data source errored and had conditions" dry-run row stays plain "Dry run", because it is not a condition mismatch.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3my32goxkwe22

loop:pushed run=contribute-20261023-212046-3my32goxkwe22 sha=97c2cfb6b681759e85bf22acce44c30e9fe335b8

Verdict: merge. The branch replaces the raw HTTP code in the Status column of all three delivery-log tables (an automation's log, the owner's logs browser, the admin logs browser) with words: each action's registry successLabel, "Skipped by condition", "Skipped", "Scheduled", named failures, "In progress", and "Dry run..." variants. The code stays available as an "HTTP nnn" tooltip. pnpm run check is clean, and pnpm run test passes (161 files, 2517 tests).

What I checked:

  • I compared the issue (F4, plus the brainstorm's M2 notes) with the code. All three places that printed statusCode (DeliveryLog.tsx, LogsBrowser.tsx, AdminLogsBrowser.tsx) now use deliveryStatusText. No other UI shows a raw code: the automation page's activity badge already used words. The diff stays within the issue's scope.
  • I checked the 204 rule against every place that writes a 204 row: run-steps.ts (fetch/search limit rows carry an error; gate, branch, loop and skip rows carry a message), handler.ts logDrySkip, follow, calendar-rsvp, collect-links and the park-suspended duplicate. Each one carries a message or an error, so reading a bare 204 as a remote's success (for example a Discord webhook) is sound. The "Scheduled action " prefix matches lib/actions/schedule.ts, and the legacy per-source skip prefixes match handler.ts.
  • Steps-model rows resolve their outcome by step id, legacy rows by action position, and admin rows server-side. This is the same rule logRowLabel already uses. The new unit and island tests cover the 2xx, skip, failure, in-progress, dry-run and tooltip cases.
  • Things the owner may want to know, none of them blocking:
    • Live mode does not yet write condition-gate rows (that is issue #14), so for now "Skipped by condition" only appears as "Dry run: skipped by condition".
    • The cocore codes 461–466 and 561–565 read plain "Failed"; the expanded row still shows the error text.

loop:verdict run=review-20261023-212614-3my32goxkwe22 sha=97c2cfb6b681759e85bf22acce44c30e9fe335b8 verdict=merge

Merged main into the branch: 46cdf533a3d2d4a5c4a99411c6bf1c7c8baabd22.

loop:carry from=97c2cfb6b681759e85bf22acce44c30e9fe335b8 sha=46cdf533a3d2d4a5c4a99411c6bf1c7c8baabd22

Merged main into the branch: 7a542fb06ce9ee6cafa93214bd16fa97a7364cac.

loop:carry from=46cdf533a3d2d4a5c4a99411c6bf1c7c8baabd22 sha=7a542fb06ce9ee6cafa93214bd16fa97a7364cac

Merged onto main as 945c7027e01f4d2ee873c82de51b0b917981f3d2.

loop:merged sha=945c7027e01f4d2ee873c82de51b0b917981f3d2 head=7a542fb06ce9ee6cafa93214bd16fa97a7364cac

Sign up or Sign in to add to the discussion.