# 2026-09-17 — `Retry` moves in after all

[2026-09-11](2026-09-11.md) examined an application's private `Retry` and kept it out of this package, for three
reasons. This session moved it in. Here is what changed about each reason, and the one that did not.

## "It reads configuration, so it is not a utility"

Still true, and it still keeps `Retry` out of `utils/`. But `utils/` was never the only place it could go. `core/` is
the layer that _behaves_: `Logger` resolves `logging` on every line, and nobody calls `Logger` a capability for it.
`Retry` resolves `delivery` the same way and sits beside it.

The capability reading had the relationship backwards. A capability is something the kernel names and cannot provide
itself, such as a cache or a queue. Retrying needs no platform resource. The kernel had already declared the retry
policy, `delivery.retries` and `delivery.retryBackoffMs`, and simply had no code that read it. A policy in one package
with its only implementation somewhere else is a split worth removing.

## "It depends on an HTTP client's error and on the application's own validation error"

Both couplings were real, and neither came across.

- **The HTTP error** is recognised by shape: an `Error` carrying a numeric `status`. `@bayudwiyansatria/web-worker`'s
  `HttpError` has exactly that, so the judgement is unchanged for errors it raises, and the kernel still imports no HTTP
  library. Any client with the same shape gets the same treatment, which is what a transport-neutral default should do.
- **The validation error** stays in the application, which passes its own `isRetryable`: refuse a validation failure,
  and defer to `Retry.isRetryable` for everything else. That was the one piece of the old judgement that knew anything
  about the application.

One side effect: the old implementation logged under a hard-coded event name. That became an option, `event`, defaulting
to `'retry'`. A caller adopting this can pass its old name so nothing querying the logs goes quiet.

## "It has one consumer" — still true

This is the reason that stands, and the move went ahead anyway. What outweighed it: the kernel has published a retry
setting with no reader since 1.1.0, so the next consumer to honour `delivery.retries` would otherwise write a second
loop, and the two private copies of `chunk` had disagreed before anyone noticed.

That is a judgement call, not a rule, so it is written down. If a second consumer never appears, this is the entry to
revisit.

## Small behaviours settled on the way

- **An attempt count below one runs once**, with `NaN` included, matching how `List.chunk` treats an unusable size. The
  old code used `Math.max(attempts, 1)`, and `Math.max(NaN, 1)` is `NaN`: the loop never ran, the operation was never
  called, and the call threw `undefined`.
- **`delivery` is resolved only when a default is used.** A call overriding both `attempts` and `backoffMs` works before
  `configure()`. The old code resolved on every call.

## Consumer adoption

The consumer was switched over and verified against a packed tarball of this working tree before publication, then
re-verified against the registry version once 1.3.1 was published. Both builds produced the same bundle size.
