H6: durable activity dedup — same :id processed at most once, ever (TDD)

Failing tests first (3 red: a redelivered activity reran its behavior — behavior/process starts
from an empty trace, so dedup evaporated per call). host/blog--process-local! now atomically
claims the :id on persist stream 'activities:processed' via ev/book! (the same append-expect
acquire as seats/votes) and returns a :deduped trace on duplicates. Store-backed → survives outbox
retries AND restarts. Prerequisite for non-idempotent effects (payment). Id-less activities process
unchecked.

blog suite 250/250 (+3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 10:45:55 +00:00
parent edbb2d4a37
commit f8b96b3d81
2 changed files with 44 additions and 2 deletions

View File

@@ -385,10 +385,22 @@
(when (and v (= (type-of v) "list")) (set! host/blog--pending-log v)))))
;; P2/TA-live: process an activity through the seam locally (fire behaviors + record suspensions).
;; Shared by emit! (our own state changes) and receive! (a peer's, arriving via /inbox).
;; H6: DURABLE idempotency — an activity :id is processed AT MOST ONCE, ever. The id is claimed
;; atomically on the persist stream "activities:processed" (ev/book! — the same append-expect
;; acquire as seats/votes), so outbox redelivery and restart replay can't rerun behaviors. This is
;; the prerequisite for non-idempotent effects (payment). An id-less activity processes unchecked.
(define host/blog--processed-stream "activities:processed")
(define host/blog--claim-activity?
(fn (aid)
(or (nil? aid) (= aid "")
(= (get (ev/book! host/blog-store host/blog--processed-stream 1000000000 aid) :status) :booked))))
(define host/blog--process-local!
(fn (a)
(let ((tr (behavior/process host/blog--publish-engine a)))
(begin (for-each (fn (s) (host/blog--record-pending! a s)) (get tr :suspended)) tr))))
(if (not (host/blog--claim-activity? (get a :id)))
{:emitted (list) :ran (list) :effects (list) :suspended (list) :failed (list)
:seen (list (get a :id)) :deduped true}
(let ((tr (behavior/process host/blog--publish-engine a)))
(begin (for-each (fn (s) (host/blog--record-pending! a s)) (get tr :suspended)) tr)))))
;; ── TA-live: the durable OUTBOX (fed-sx reliability) ──────────────────
;; Emitted activities are QUEUED per-peer (durable) and delivered BEST-EFFORT. A peer being DOWN
;; does NOT fail the local emit — delivery is GUARDED, and a failed item stays queued for retry (on