host P0.3b: durable flow log — survives restart (LIVE-VERIFIED)

The driver now persists each effect record to the blog store (string-keyed to dodge the keyword/
persist top-level split), and host/blog-load-flowlog! rebuilds the in-memory log on boot (wired into
serve.sh after load-edges!). So /flows survives a restart — closing the P0.3 gap.

LIVE PROOF: published a post on blog.rose-ash.com → /flows showed validate+notify → RESTARTED the
container (in-memory log lost) → /flows STILL showed them, reloaded from the durable store.
Round-trip also covered by a conformance test (persist → clear → reload → identical). blog 208/208,
full host conformance 599/599. Note: whole-list rewrite per effect — fine at P0 volume, cap/rotate later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:01:10 +00:00
parent 9ac6a8afd5
commit 67d2fad8d8
4 changed files with 39 additions and 5 deletions

View File

@@ -161,12 +161,22 @@
{:register! (fn (spec dag hint) nil)
:match (fn (a) (if (and (= (get a :type) "create") (= (get (get a :object) :type) "article"))
(list {:dag host/blog--publish-dag}) (list)))})
;; P0.3b: the flow log is DURABLE — string-keyed records (dodge the keyword/persist top-level split),
;; persisted to the blog store under one key, so /flows survives a restart. Boot-loaded via
;; host/blog-load-flowlog!. (Whole-list rewrite per effect — fine at P0 volume; cap/rotate later.)
(define host/blog--flowlog-key "flowlog")
(define host/blog--driver
{:dispatch (fn (eff)
(begin (set! host/blog--flow-log
(concat host/blog--flow-log
(list {:verb (get eff :verb) :args (get eff :args)})))
(list)))}) ;; record the effect; no follow-up activities (P0)
(list {"verb" (get eff :verb) "args" (get eff :args)})))
(persist/backend-kv-put host/blog-store host/blog--flowlog-key host/blog--flow-log)
(list)))}) ;; record the effect (durably); no follow-up activities (P0)
;; rebuild the in-memory flow log from the durable store (call on boot, like host/blog-load-edges!).
(define host/blog-load-flowlog!
(fn ()
(let ((v (persist/backend-kv-get host/blog-store host/blog--flowlog-key)))
(when (and v (= (type-of v) "list")) (set! host/blog--flow-log v)))))
(define host/blog--publish-engine
(behavior/make-engine {:triggers host/blog--triggers :runner host/flow--exec-runner
:transport host/blog--transport :driver host/blog--driver
@@ -2667,8 +2677,8 @@
(quote (p (em "No flows yet — publish a post to fire the on-publish DAG.")))
(cons (quote ul)
(map (fn (e)
(quasiquote (li (strong (unquote (get e :verb))) " "
(unquote (if (> (len (get e :args)) 0) (str (first (get e :args))) "")))))
(quasiquote (li (strong (unquote (get e "verb"))) " "
(unquote (if (> (len (get e "args")) 0) (str (first (get e "args"))) "")))))
host/blog--flow-log))))))))))
;; ── routes ──────────────────────────────────────────────────────────

View File

@@ -149,6 +149,11 @@ EPOCH=1
echo "(epoch $EPOCH)"
echo "(eval \"(host/blog-load-edges!)\")"
EPOCH=$((EPOCH+1))
# P0.3b: rebuild the in-memory publish flow log from the durable store, so /flows
# survives a restart (the driver persists each effect record under "flowlog").
echo "(epoch $EPOCH)"
echo "(eval \"(host/blog-load-flowlog!)\")"
EPOCH=$((EPOCH+1))
# Sessions on the DURABLE store, LAZILY: only a logged-in session (one that
# writes a field) persists, so a login survives a restart while anonymous /
# crawler traffic leaves no rows. host/session-init! bumps the per-boot epoch

View File

@@ -1223,8 +1223,22 @@
(host/blog-put! "p03b" "U" "(article (h1 \"u\"))" "published")
(host/blog--set-field-values! "p03b" {"category" "urgent"})
(host/blog--maybe-publish! "p03b" nil "published")
(map (fn (e) (get e :verb)) host/blog--flow-log))
(map (fn (e) (get e "verb")) host/blog--flow-log))
(list "validate" "digest" "validate" "notify"))
;; P0.3b: the flow log is DURABLE — it round-trips through the blog store (survives a restart).
(host-bl-test "P0.3b: the flow log persists + reloads from the store (string-keyed, no split)"
(begin
(set! host/blog--flow-log (list))
(persist/backend-kv-put host/blog-store host/blog--flowlog-key (list)) ;; reset durable
(host/blog-put! "p03d" "D" "(article (h1 \"d\"))" "published")
(host/blog--set-field-values! "p03d" {"category" "newsletter"})
(host/blog--maybe-publish! "p03d" "draft" "published") ;; fires → persists
(let ((before (map (fn (e) (get e "verb")) host/blog--flow-log)))
(begin
(set! host/blog--flow-log (list)) ;; simulate a restart
(host/blog-load-flowlog!) ;; reload from the store
(list before (map (fn (e) (get e "verb")) host/blog--flow-log)))))
(list (list "validate" "digest") (list "validate" "digest")))
(define
host-bl-tests-run!

View File

@@ -214,6 +214,11 @@ covers everything until a DAG's cost/latency/placement forces the substrate.
activities), so business logic can change state, which federates, which triggers more flows.
## Progress log (newest first)
- 2026-07-02 — P0.3b DONE + LIVE-VERIFIED. The flow log is now DURABLE: the driver
persists string-keyed effect records to the blog store (dodging the keyword/persist top-level
split); host/blog-load-flowlog! rebuilds it on boot (serve.sh). Proof: published on
blog.rose-ash.com, RESTARTED the container, /flows still showed validate+notify (reloaded from the
store). blog 208/208, conformance 599/599. Whole-list rewrite per effect — cap/rotate later.
- 2026-07-02 — P0.3 DONE + LIVE-VERIFIED. The seam wired into the live publish path: on-publish
registry + in-process transport + host driver + the execute-fold runner, fired by the draft→
published transition in both write handlers. Published a real post on blog.rose-ash.com → /flows