From 67d2fad8d8a3ad4eec7a109214e862b9f0a91ede Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 2 Jul 2026 15:01:10 +0000 Subject: [PATCH] =?UTF-8?q?host=20P0.3b:=20durable=20flow=20log=20?= =?UTF-8?q?=E2=80=94=20survives=20restart=20(LIVE-VERIFIED)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/host/blog.sx | 18 ++++++++++++++---- lib/host/serve.sh | 5 +++++ lib/host/tests/blog.sx | 16 +++++++++++++++- plans/business-logic-fed-flows.md | 5 +++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/lib/host/blog.sx b/lib/host/blog.sx index 5c144574..4dec2245 100644 --- a/lib/host/blog.sx +++ b/lib/host/blog.sx @@ -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 ────────────────────────────────────────────────────────── diff --git a/lib/host/serve.sh b/lib/host/serve.sh index ac54c8c9..7a9d30f6 100755 --- a/lib/host/serve.sh +++ b/lib/host/serve.sh @@ -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 diff --git a/lib/host/tests/blog.sx b/lib/host/tests/blog.sx index 0b83b8d3..3f9deaac 100644 --- a/lib/host/tests/blog.sx +++ b/lib/host/tests/blog.sx @@ -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! diff --git a/plans/business-logic-fed-flows.md b/plans/business-logic-fed-flows.md index 91455b97..f14e5ddf 100644 --- a/plans/business-logic-fed-flows.md +++ b/plans/business-logic-fed-flows.md @@ -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