Refactor delivery targets to a Target interface (closes #77) #81

Merged
sneak merged 1 commits from issue-77-target-interface into main 2026-08-07 17:07:49 +02:00
Collaborator

Refactors the delivery engine so each target TYPE is an implementation of a Target interface, dispatched from a registry, with each target owning its full delivery including durable retries. Implements the authoritative design from issue #77 (the corrected "hand the DB + Scheduler to the target" design).

The new interface

type Scheduler interface {
    ScheduleRetry(task Task, delay time.Duration)
}

type Target interface {
    Deliver(ctx context.Context, webhookDB *gorm.DB,
        d *database.Delivery, task *Task, sched Scheduler)
}

Deliver receives everything a target needs to be autonomous and durable: the request context, the per-webhook *gorm.DB, the *database.Delivery, the attempt *Task, and a Scheduler (the engine) for durable re-enqueue. The target makes one attempt, writes the DeliveryResult, updates DeliveryStatus, and — for retry targets — decides whether to retry, computes its own backoff, gates with its own circuit breaker, and reschedules via the injected Scheduler.

processDelivery collapses to a registry lookup (map[database.TargetType]Target) and a Deliver call; an unknown target type still fails the delivery as before.

Per-target ownership

  • httpTarget and slackTarget share a retry core (httpCore) that owns retry, exponential backoff, and the per-target circuit breaker. The core is fire-and-forget when MaxRetries == 0 and adds breaker-gated backed-off retries when MaxRetries > 0. The per-attempt request differs (HTTP forwards the body + filtered headers; Slack posts a formatted message) and is supplied as a closure, so each keeps its exact recording semantics (e.g. HTTP records no error string for a non-2xx, Slack records HTTP <code>).
  • databaseTarget and logTarget are fire-and-forget: they record a single successful attempt.

Moved wholesale into the http/slack targets: deliverHTTP*, handleHTTPRetry, circuitBreakerBlock, calcBackoff / calcRemainingBackoff / backoffElapsed, the circuit-breaker sync.Map + getCircuitBreaker, clientForConfig, doHTTPRequest, applyRequestHeaders, and the config parsers. The engine keeps recordResult, updateDeliveryStatus, and ScheduleRetry.

Slack MaxRetries gating

Slack is now on the same shared core as HTTP, with retry + breaker gated on MaxRetries. A MaxRetries of 0 stays single-attempt fire-and-forget, so every existing Slack target is unchanged; a Slack target configured with retries gets backoff + circuit breaker.

Log-target full content

logTarget now logs the ENTIRE inbound webhook — full request body and full request headers, plus method, content type, and the webhook id and entrypoint id — rather than a summary line. This supersedes the smaller log-summary work (#70).

Task.EntrypointID

To carry the entrypoint id to the log target, Task gains an EntrypointID field, populated in the webhook handler's buildDeliveryTasks, the engine's recovery-task builder, and buildEventFromTask.

Durability / recovery

The crash-durable async retry model is preserved unchanged: one attempt per worker turn; on failure the status is set retrying, backoff is computed, and the task is re-enqueued via ScheduleRetry (a time.AfterFunc onto the retry channel). On restart, recoverRetryingDeliveries and the 60s sweep hand each orphaned retrying delivery back to its target to recompute the remaining backoff and reschedule (targets that own retries implement an internal rescheduler; fire-and-forget targets, which never produce retrying deliveries, are skipped).

How behaviour is preserved

No external behaviour changes except the two called out above (log target full content; Slack gaining MaxRetries-gated retries). All existing delivery tests pass with only their export_test.go wrappers re-pointed at the new structure — ExportDeliverHTTP/Slack/Database/Log now call the targets, ExportGetCircuitBreaker / ExportClient / ExportClientForConfig / ExportDoHTTPRequest resolve against the HTTP target's shared client and breaker map, and ExportParseHTTPConfig / ExportParseSlackConfig call the relocated free functions. Added: a logTarget test asserting the log line contains the full body, headers, and ids, and a Slack MaxRetries-gated retry test.

docker build . is green (fmt-check, lint, test, static build all pass).

Closes #77

Refactors the delivery engine so each target TYPE is an implementation of a `Target` interface, dispatched from a registry, with each target owning its full delivery including durable retries. Implements the authoritative design from issue #77 (the corrected "hand the DB + Scheduler to the target" design). ## The new interface ```go type Scheduler interface { ScheduleRetry(task Task, delay time.Duration) } type Target interface { Deliver(ctx context.Context, webhookDB *gorm.DB, d *database.Delivery, task *Task, sched Scheduler) } ``` `Deliver` receives everything a target needs to be autonomous and durable: the request context, the per-webhook `*gorm.DB`, the `*database.Delivery`, the attempt `*Task`, and a `Scheduler` (the engine) for durable re-enqueue. The target makes one attempt, writes the `DeliveryResult`, updates `DeliveryStatus`, and — for retry targets — decides whether to retry, computes its own backoff, gates with its own circuit breaker, and reschedules via the injected `Scheduler`. `processDelivery` collapses to a registry lookup (`map[database.TargetType]Target`) and a `Deliver` call; an unknown target type still fails the delivery as before. ## Per-target ownership - `httpTarget` and `slackTarget` share a retry core (`httpCore`) that owns retry, exponential backoff, and the per-target circuit breaker. The core is fire-and-forget when `MaxRetries == 0` and adds breaker-gated backed-off retries when `MaxRetries > 0`. The per-attempt request differs (HTTP forwards the body + filtered headers; Slack posts a formatted message) and is supplied as a closure, so each keeps its exact recording semantics (e.g. HTTP records no error string for a non-2xx, Slack records `HTTP <code>`). - `databaseTarget` and `logTarget` are fire-and-forget: they record a single successful attempt. Moved wholesale into the http/slack targets: `deliverHTTP*`, `handleHTTPRetry`, `circuitBreakerBlock`, `calcBackoff` / `calcRemainingBackoff` / `backoffElapsed`, the circuit-breaker `sync.Map` + `getCircuitBreaker`, `clientForConfig`, `doHTTPRequest`, `applyRequestHeaders`, and the config parsers. The engine keeps `recordResult`, `updateDeliveryStatus`, and `ScheduleRetry`. ## Slack MaxRetries gating Slack is now on the same shared core as HTTP, with retry + breaker gated on `MaxRetries`. A `MaxRetries` of 0 stays single-attempt fire-and-forget, so **every existing Slack target is unchanged**; a Slack target configured with retries gets backoff + circuit breaker. ## Log-target full content `logTarget` now logs the ENTIRE inbound webhook — full request body and full request headers, plus method, content type, and the webhook id and entrypoint id — rather than a summary line. This supersedes the smaller log-summary work (#70). ## `Task.EntrypointID` To carry the entrypoint id to the log target, `Task` gains an `EntrypointID` field, populated in the webhook handler's `buildDeliveryTasks`, the engine's recovery-task builder, and `buildEventFromTask`. ## Durability / recovery The crash-durable async retry model is preserved unchanged: one attempt per worker turn; on failure the status is set `retrying`, backoff is computed, and the task is re-enqueued via `ScheduleRetry` (a `time.AfterFunc` onto the retry channel). On restart, `recoverRetryingDeliveries` and the 60s sweep hand each orphaned `retrying` delivery back to its target to recompute the remaining backoff and reschedule (targets that own retries implement an internal `rescheduler`; fire-and-forget targets, which never produce `retrying` deliveries, are skipped). ## How behaviour is preserved No external behaviour changes except the two called out above (log target full content; Slack gaining `MaxRetries`-gated retries). All existing delivery tests pass with only their `export_test.go` wrappers re-pointed at the new structure — `ExportDeliverHTTP/Slack/Database/Log` now call the targets, `ExportGetCircuitBreaker` / `ExportClient` / `ExportClientForConfig` / `ExportDoHTTPRequest` resolve against the HTTP target's shared client and breaker map, and `ExportParseHTTPConfig` / `ExportParseSlackConfig` call the relocated free functions. Added: a `logTarget` test asserting the log line contains the full body, headers, and ids, and a Slack `MaxRetries`-gated retry test. `docker build .` is green (fmt-check, lint, test, static build all pass). Closes #77
clawbot added 1 commit 2026-08-07 16:43:49 +02:00
Refactor delivery targets to a Target interface (closes #77)
All checks were successful
check / check (push) Successful in 5s
7b1f997194
Each target TYPE is now an implementation of a Target interface,
dispatched from a registry in processDelivery instead of a type
switch on TargetType. Every target owns its full delivery,
including durable retries.

- Target.Deliver receives the context, the per-webhook DB, the
  Delivery, the attempt Task, and a Scheduler for durable
  re-enqueue (the existing timer + retry queue). The target makes
  one attempt, records the DeliveryResult, updates DeliveryStatus,
  and — for retry targets — decides whether to retry, computes its
  own backoff, gates with its own circuit breaker, and reschedules
  via the Scheduler.
- httpTarget and slackTarget share a retry core (retry, backoff,
  circuit breaker). database and log targets are fire-and-forget.
- Slack retry/breaker is gated on MaxRetries: 0 stays
  fire-and-forget (existing Slack targets unchanged), >0 gets
  retry + backoff + breaker on the shared core.
- The engine keeps only the worker pool, queue/channels, restart
  recovery/sweep, the recordResult/updateDeliveryStatus helpers,
  and ScheduleRetry. Recovery/sweep hand each orphaned retrying
  delivery back to its target to recompute the backoff.
- The log target logs the entire inbound webhook: full body and
  headers, method, content type, and the webhook and entrypoint
  ids (supersedes the smaller log-summary work).
- Task gains EntrypointID, populated in the webhook handler, the
  recovery-task builder, and buildEventFromTask.

Behaviour is preserved: existing delivery tests pass with their
export_test wrappers re-pointed at the new targets; new pure
Deliver tests cover the log full-content output and the gated
Slack retry path.
Author
Collaborator

File-by-file change summary:

  • internal/delivery/target.go (new): the Scheduler and Target interfaces, the internal rescheduler interface (used by recovery/sweep to let a target recompute its backoff), the attemptResult struct, and initTargets which builds the map[database.TargetType]Target registry and wires the shared SSRF-safe client into the http/slack targets.
  • internal/delivery/target_http.go (new): HTTPTargetConfig, the shared httpCore (fire-and-forget vs breaker-gated retry orchestration, getCircuitBreaker + the breaker sync.Map, handleRetry, circuitBreakerBlock, remainingBackoff/backoffElapsed, calcBackoff), and httpTarget (Deliver, attempt, doHTTPRequest, clientForConfig, applyRequestHeaders, isForwardableHeader, executeHTTPRequest, parseHTTPConfig).
  • internal/delivery/target_slack.go (new): SlackTargetConfig, slackTarget (Deliver, attempt, readSlackResponse, failConfig), parseSlackConfig, and the message formatting (FormatSlackMessage, formatJSONBody, formatRawBody). Slack runs on the shared httpCore, gated on MaxRetries.
  • internal/delivery/target_database.go (new): databaseTarget — fire-and-forget, records one success.
  • internal/delivery/target_log.go (new): logTarget — logs the full body, full headers, method, content type, webhook id and entrypoint id, then records one success.
  • internal/delivery/engine.go: removed the per-type delivery methods and the type switch; processDelivery is now a registry lookup + Deliver. Removed the engine's client field and circuit-breaker map (moved into the targets); New and the test constructors call initTargets. recoverSingleRetry/sweepSingleRetry now delegate the backoff recompute to the delivery's target via rescheduler. Renamed scheduleRetry to the exported ScheduleRetry (the Scheduler impl). Added Task.EntrypointID, populated in buildEventFromTask and buildRecoveryTask.
  • internal/delivery/export_test.go: re-pointed the wrappers at the new structure (ExportDeliverHTTP/Slack/Database/Log call the targets; ExportGetCircuitBreaker/ExportClient/ExportClientForConfig/ExportDoHTTPRequest resolve against the http target; ExportParseHTTPConfig/ExportParseSlackConfig call the relocated free functions); the test engine constructors now call initTargets.
  • internal/delivery/engine_test.go: added TestDeliverLog_LogsFullContent (asserts the log line carries the full body, headers, and ids) and TestDeliverSlack_WithRetries_SchedulesRetry (the gated Slack retry path), plus small test helpers.
  • internal/handlers/webhook.go: populate Task.EntrypointID from entrypoint.ID in buildDeliveryTasks.

Validation:

docker build . -t webhooker-issue77  ->  exit code: 0

(fmt-check, lint, full test suite, and the static build all pass inside the image.)

File-by-file change summary: - `internal/delivery/target.go` (new): the `Scheduler` and `Target` interfaces, the internal `rescheduler` interface (used by recovery/sweep to let a target recompute its backoff), the `attemptResult` struct, and `initTargets` which builds the `map[database.TargetType]Target` registry and wires the shared SSRF-safe client into the http/slack targets. - `internal/delivery/target_http.go` (new): `HTTPTargetConfig`, the shared `httpCore` (fire-and-forget vs breaker-gated retry orchestration, `getCircuitBreaker` + the breaker `sync.Map`, `handleRetry`, `circuitBreakerBlock`, `remainingBackoff`/`backoffElapsed`, `calcBackoff`), and `httpTarget` (`Deliver`, `attempt`, `doHTTPRequest`, `clientForConfig`, `applyRequestHeaders`, `isForwardableHeader`, `executeHTTPRequest`, `parseHTTPConfig`). - `internal/delivery/target_slack.go` (new): `SlackTargetConfig`, `slackTarget` (`Deliver`, `attempt`, `readSlackResponse`, `failConfig`), `parseSlackConfig`, and the message formatting (`FormatSlackMessage`, `formatJSONBody`, `formatRawBody`). Slack runs on the shared `httpCore`, gated on `MaxRetries`. - `internal/delivery/target_database.go` (new): `databaseTarget` — fire-and-forget, records one success. - `internal/delivery/target_log.go` (new): `logTarget` — logs the full body, full headers, method, content type, webhook id and entrypoint id, then records one success. - `internal/delivery/engine.go`: removed the per-type delivery methods and the type switch; `processDelivery` is now a registry lookup + `Deliver`. Removed the engine's `client` field and circuit-breaker map (moved into the targets); `New` and the test constructors call `initTargets`. `recoverSingleRetry`/`sweepSingleRetry` now delegate the backoff recompute to the delivery's target via `rescheduler`. Renamed `scheduleRetry` to the exported `ScheduleRetry` (the `Scheduler` impl). Added `Task.EntrypointID`, populated in `buildEventFromTask` and `buildRecoveryTask`. - `internal/delivery/export_test.go`: re-pointed the wrappers at the new structure (`ExportDeliverHTTP/Slack/Database/Log` call the targets; `ExportGetCircuitBreaker`/`ExportClient`/`ExportClientForConfig`/`ExportDoHTTPRequest` resolve against the http target; `ExportParseHTTPConfig`/`ExportParseSlackConfig` call the relocated free functions); the test engine constructors now call `initTargets`. - `internal/delivery/engine_test.go`: added `TestDeliverLog_LogsFullContent` (asserts the log line carries the full body, headers, and ids) and `TestDeliverSlack_WithRetries_SchedulesRetry` (the gated Slack retry path), plus small test helpers. - `internal/handlers/webhook.go`: populate `Task.EntrypointID` from `entrypoint.ID` in `buildDeliveryTasks`. Validation: docker build . -t webhooker-issue77 -> exit code: 0 (fmt-check, lint, full test suite, and the static build all pass inside the image.)
sneak merged commit 81413c56e9 into main 2026-08-07 17:07:49 +02:00
sneak deleted branch issue-77-target-interface 2026-08-07 17:07:49 +02:00
Author
Collaborator

Critical review: PR #81 — Refactor delivery targets to a Target interface

Verdict: PASS (merge-ready).

Reviewed the full diff of all 9 changed files against origin/main, compared every new target implementation line-by-line against the old Engine methods it replaces, built the container, and ran the delivery + handlers test suites.

What was verified

Behaviour preservation (retry/backoff/breaker/status/attempt numbers):

  • httpTarget fire-and-forget path is byte-for-byte equivalent to the old deliverHTTPFireAndForget: records attempt 1, success = reqErr == nil &amp;&amp; 200 &lt;= code &lt; 300, empty error string on a non-2xx, Delivered/Failed transitions unchanged.
  • httpCore.withRetry reproduces deliverHTTPWithRetry exactly: breaker gate → recordResult(attemptNum)RecordSuccess+Delivered, or RecordFailure+handleRetry. handleRetry matches handleHTTPRetry (attemptNum &gt;= maxRetriesFailed; else Retrying, calcBackoff(attemptNum), AttemptNum+1). circuitBreakerBlock is identical, including re-scheduling the same-attempt task with the cooldown remaining.
  • Slack: for the existing MaxRetries == 0 case the new fireAndForget path is equivalent to the old deliverSlack/sendSlackRequest/handleSlackResponse/failSlackDelivery — attempt 1, HTTP &lt;code&gt; on non-2xx, sending request: … on transport error, same Content-Type/User-Agent headers, same shared SSRF-safe client. The new MaxRetries &gt; 0 retry+breaker path is the intended, explicitly-scoped new capability and does not affect existing (0-retry) Slack targets.
  • database/log fire-and-forget: unchanged (record one success, mark Delivered). log now emits the full body, full headers, method, content type, and webhook + entrypoint ids (supersedes the old body_length-only line), as required.
  • calcBackoff / remainingBackoff / backoffElapsed are identical to the old free/engine functions.

Circuit breaker relocation: Engine.ExportGetCircuitBreaker now resolves against e.httpTarget.getCircuitBreaker, i.e. the same map the HTTP target actually uses, so breaker-reaching tests address the correct breaker. HTTP and Slack owning separate breaker maps is correct: a target id belongs to exactly one target type, so the maps never key the same id from two owners — no collision.

Recovery/sweep delegation: recovery and the 60s sweep now load the target, look it up in the registry, and skip anything not implementing the internal rescheduler. This is safe: the only code paths that ever set status retrying are the HTTP/Slack retry cores, and both implement rescheduler. Fire-and-forget targets provably never produce retrying deliveries, so nothing the old code would have rescheduled is silently dropped.

Task.EntrypointID: populated in all three builders — handler buildDeliveryTasks (entrypoint.ID), engine buildRecoveryTask (event.EntrypointID), and buildEventFromTask (task.EntrypointID).

Quality bar: single clean commit authored as sneak; no AI/tooling references anywhere in the diff, commit message, or comments. Naming is consistent and stutter-free; error handling and logging preserved. New tests are meaningful: the log full-content test seeds real non-empty webhook/entrypoint UUIDs and asserts body marker, headers, content type, and both ids appear (non-vacuous), and asserts Delivered; the Slack gated-retry test drives a 503 server with MaxRetries: 5 and asserts Retrying status plus a recorded failed 503 result. Existing tests pass with only export_test.go wrappers re-pointed at the new structure, matching the stated design.

Build/tests: docker build exits 0 (Dockerfile runs make fmt-check, make lint, make test, make build; cache is content-keyed on the source copy, so the hit confirms this exact tree passed). Ran the tests directly to confirm: internal/delivery ok, internal/handlers ok.

Non-blocking observation (informational, low severity)

The recovery/sweep rescheduler skip means an orphaned retrying delivery whose target had its type mutated in the DB from a retry type (http/slack) to a fire-and-forget type (or to an unknown type) would now stay stuck in retrying, where the old code would eventually re-dispatch it and let the new type mark it Delivered/Failed. This requires mutating a target's type while a retrying delivery for it is orphaned — an extremely narrow operational edge case that neither implementation handles "correctly," and it does not arise in normal operation. Noted only for completeness; not a merge blocker.

## Critical review: PR #81 — Refactor delivery targets to a Target interface **Verdict: PASS (merge-ready).** Reviewed the full diff of all 9 changed files against `origin/main`, compared every new target implementation line-by-line against the old `Engine` methods it replaces, built the container, and ran the delivery + handlers test suites. ### What was verified **Behaviour preservation (retry/backoff/breaker/status/attempt numbers):** - `httpTarget` fire-and-forget path is byte-for-byte equivalent to the old `deliverHTTPFireAndForget`: records attempt 1, success = `reqErr == nil &amp;&amp; 200 &lt;= code &lt; 300`, empty error string on a non-2xx, `Delivered`/`Failed` transitions unchanged. - `httpCore.withRetry` reproduces `deliverHTTPWithRetry` exactly: breaker gate → `recordResult(attemptNum)` → `RecordSuccess`+`Delivered`, or `RecordFailure`+`handleRetry`. `handleRetry` matches `handleHTTPRetry` (`attemptNum &gt;= maxRetries` → `Failed`; else `Retrying`, `calcBackoff(attemptNum)`, `AttemptNum+1`). `circuitBreakerBlock` is identical, including re-scheduling the same-attempt task with the cooldown remaining. - Slack: for the existing `MaxRetries == 0` case the new `fireAndForget` path is equivalent to the old `deliverSlack`/`sendSlackRequest`/`handleSlackResponse`/`failSlackDelivery` — attempt 1, `HTTP &lt;code&gt;` on non-2xx, `sending request: …` on transport error, same Content-Type/User-Agent headers, same shared SSRF-safe client. The new `MaxRetries &gt; 0` retry+breaker path is the intended, explicitly-scoped new capability and does not affect existing (0-retry) Slack targets. - `database`/`log` fire-and-forget: unchanged (record one success, mark `Delivered`). `log` now emits the full body, full headers, method, content type, and webhook + entrypoint ids (supersedes the old `body_length`-only line), as required. - `calcBackoff` / `remainingBackoff` / `backoffElapsed` are identical to the old free/engine functions. **Circuit breaker relocation:** `Engine.ExportGetCircuitBreaker` now resolves against `e.httpTarget.getCircuitBreaker`, i.e. the same map the HTTP target actually uses, so breaker-reaching tests address the correct breaker. HTTP and Slack owning separate breaker maps is correct: a target id belongs to exactly one target type, so the maps never key the same id from two owners — no collision. **Recovery/sweep delegation:** recovery and the 60s sweep now load the target, look it up in the registry, and skip anything not implementing the internal `rescheduler`. This is safe: the only code paths that ever set status `retrying` are the HTTP/Slack retry cores, and both implement `rescheduler`. Fire-and-forget targets provably never produce `retrying` deliveries, so nothing the old code would have rescheduled is silently dropped. **Task.EntrypointID:** populated in all three builders — handler `buildDeliveryTasks` (`entrypoint.ID`), engine `buildRecoveryTask` (`event.EntrypointID`), and `buildEventFromTask` (`task.EntrypointID`). **Quality bar:** single clean commit authored as `sneak`; no AI/tooling references anywhere in the diff, commit message, or comments. Naming is consistent and stutter-free; error handling and logging preserved. New tests are meaningful: the log full-content test seeds real non-empty webhook/entrypoint UUIDs and asserts body marker, headers, content type, and both ids appear (non-vacuous), and asserts `Delivered`; the Slack gated-retry test drives a 503 server with `MaxRetries: 5` and asserts `Retrying` status plus a recorded failed 503 result. Existing tests pass with only `export_test.go` wrappers re-pointed at the new structure, matching the stated design. **Build/tests:** `docker build` exits 0 (Dockerfile runs `make fmt-check`, `make lint`, `make test`, `make build`; cache is content-keyed on the source copy, so the hit confirms this exact tree passed). Ran the tests directly to confirm: `internal/delivery` ok, `internal/handlers` ok. ### Non-blocking observation (informational, low severity) The recovery/sweep `rescheduler` skip means an orphaned `retrying` delivery whose target had its **type mutated in the DB** from a retry type (http/slack) to a fire-and-forget type (or to an unknown type) would now stay stuck in `retrying`, where the old code would eventually re-dispatch it and let the new type mark it `Delivered`/`Failed`. This requires mutating a target's type while a `retrying` delivery for it is orphaned — an extremely narrow operational edge case that neither implementation handles "correctly," and it does not arise in normal operation. Noted only for completeness; not a merge blocker.
clawbot added the merge-ready label 2026-08-07 17:11:37 +02:00
sneak was assigned by clawbot 2026-08-07 17:11:38 +02:00
Author
Collaborator

Manager note (separate from the review above): this PR was already merged to main by sneak before the independent critical review completed, so #77 is closed via the merge. The review (posted above) landed a PASS verdict, so no rework is needed. The one non-blocking edge case it raised — an orphaned retrying delivery whose target type is later mutated to a non-retry type would stay stuck in retrying — is captured for follow-up as #82.

Manager note (separate from the review above): this PR was already merged to `main` by `sneak` before the independent critical review completed, so #77 is closed via the merge. The review (posted above) landed a PASS verdict, so no rework is needed. The one non-blocking edge case it raised — an orphaned `retrying` delivery whose target `type` is later mutated to a non-retry type would stay stuck in `retrying` — is captured for follow-up as #82.
Author
Collaborator

Independent critical review (the review gate) — no correctness blockers

For the record: the result of the independent reviewer I dispatched separately from the author agent. This PR was merged by @sneak before it finished, so the gate ran post-merge; it confirms the merged code is sound.

Verdict: no behavioural regressions. Verified line-by-line against the pre-refactor engine:

  • HTTP retries preserved: same attempt numbering, same terminal condition at maxRetries, same exponential backoff (calcBackoff, shift capped), same next-attempt increment; fire-and-forget at a MaxRetries of 0 records one attempt unchanged.
  • Circuit breaker preserved: same keying by target id, same Allow/RecordSuccess/RecordFailure points; breaker-open writes no result row, sets retrying, and reschedules the same attempt with the cooldown remaining.
  • Recovery/sweep preserved: same attempt-count and remaining-backoff arithmetic, delegated to the target; the unchanged recovery integration tests still pass. Fire-and-forget targets are correctly skipped (they never produce retrying rows).
  • Existing Slack targets (a MaxRetries of 0) unchanged: single attempt, no breaker, same result recording and headers. The new retry+breaker path applies only when retries are configured.
  • Log target logs the full body and full headers plus ids; Task.EntrypointID populated on every path; the SSRF-safe transport is on every outbound path; exactly one result row per attempt (zero on breaker-open); no AI/tooling references; docker build green.

One process caveat (not correctness): the branch was 2 commits behind main (#78 retention reaper, #75 NoCache), so the refactor was never built together with those. A merge-tree check showed the merge is conflict-free and drops neither, and @sneak's merge is a real merge commit — so nothing was lost. The integrated main build will be exercised by the next work units branching from it.

Net: the independent gate agrees with the on-PR review — the refactor is sound. Follow-up #82 tracks the one narrow, non-blocking edge case.

## Independent critical review (the review gate) — no correctness blockers For the record: the result of the independent reviewer I dispatched separately from the author agent. This PR was merged by @sneak before it finished, so the gate ran post-merge; it confirms the merged code is sound. Verdict: no behavioural regressions. Verified line-by-line against the pre-refactor engine: - HTTP retries preserved: same attempt numbering, same terminal condition at `maxRetries`, same exponential backoff (`calcBackoff`, shift capped), same next-attempt increment; fire-and-forget at a `MaxRetries` of 0 records one attempt unchanged. - Circuit breaker preserved: same keying by target id, same `Allow`/`RecordSuccess`/`RecordFailure` points; breaker-open writes no result row, sets `retrying`, and reschedules the same attempt with the cooldown remaining. - Recovery/sweep preserved: same attempt-count and remaining-backoff arithmetic, delegated to the target; the unchanged recovery integration tests still pass. Fire-and-forget targets are correctly skipped (they never produce `retrying` rows). - Existing Slack targets (a `MaxRetries` of 0) unchanged: single attempt, no breaker, same result recording and headers. The new retry+breaker path applies only when retries are configured. - Log target logs the full body and full headers plus ids; `Task.EntrypointID` populated on every path; the SSRF-safe transport is on every outbound path; exactly one result row per attempt (zero on breaker-open); no AI/tooling references; docker build green. One process caveat (not correctness): the branch was 2 commits behind `main` (#78 retention reaper, #75 NoCache), so the refactor was never built together with those. A merge-tree check showed the merge is conflict-free and drops neither, and @sneak's merge is a real merge commit — so nothing was lost. The integrated `main` build will be exercised by the next work units branching from it. Net: the independent gate agrees with the on-PR review — the refactor is sound. Follow-up #82 tracks the one narrow, non-blocking edge case.
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#81