RA-live: durable business logic in production — host drives the kernel service (LIVE)

Steps 1+2 of RA-live/TA-live, live-verified end-to-end on blog.rose-ash.com.

(1) DEPLOY: docker-compose.dev-sx-host.yml gains an sx_kernel service running next/kernel/serve.sh
(the durable-execution kernel), SX_HTTP_HOST=0.0.0.0 so the host container reaches it at
http://sx_kernel:8930.

(2) HOST AS CLIENT: lib/host/ra.sx gains a KERNEL runner — host/ra--make-kernel-runner drives the
kernel over HTTP (http-request, native primitive; returns {status headers body}). It advertises
{effect,branch,each,suspend}, so select-runner routes a durable DAG to it. host/blog.sx: the DAG
registry + runner fleet are now mutable (register-dag!/add-runner!); emit! records SUSPENSIONS in a
durable pending log; /flows shows suspended instances with a resume link (?resume=<id>) driving
host/ra--kernel-resume. serve.sh wires it: set kernel-base, add the kernel runner, register the
durable 'blog-digest' DAG, declare a DURABLE behavior on article (create→publish SYNC, update→
blog-digest DURABLE), add a 'category' field.

LIVE PROOF: editing a published newsletter article → Update → routes to the kernel runner → POST
/flow/start/newsletter → kernel SUSPENDS (instance 5, shown pending on /flows) → /flows?resume=5 →
host re-drives the kernel → DONE → digest-sent effect + pending cleared. Durable suspend/resume
across separate HTTP requests, on a deployed persistent kernel. urgent edits complete immediately
(digest). http-request works in the serving context. blog 217/217, full conformance green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 19:31:33 +00:00
parent c0d9cb3cf4
commit cb0d866002
4 changed files with 149 additions and 13 deletions

View File

@@ -61,3 +61,37 @@
;; the REAL erl-eval — ONLY works where the Erlang runtime + next/flow are loaded (refs resolved
;; lazily at call time, so defining it here is harmless in the plain host).
(define host/ra--real-eval (fn (src) (er-to-sx-deep (erlang-eval-ast src))))
;; ── RA-LIVE: drive the persistent durable-execution KERNEL SERVICE over HTTP ──────────
;; next/kernel/host_kernel serves GET /flow/start/<category> → "<instance-id>:<status>" and
;; GET /flow/resume/<id> → "resume:<status>". http-request returns {"status" "headers" "body"};
;; we take the body. So RA-live's runner is the SAME seam contract, executing durably ON A REMOTE
;; PROCESS — a suspend returns the kernel instance id as the :resume handle for a later resume.
(define host/ra--http-get (fn (url) (get (http-request "GET" url {} "") "body")))
(define host/ra--parse-kernel
(fn (body)
(let ((i (index-of body ":")))
(if (< i 0) {:status "failed" :error body}
(let ((id (substr body 0 i)) (status (substr body (+ i 1) (- (len body) (+ i 1)))))
(cond
((starts-with? status "suspended") {:status "suspended" :resume {:id id}})
((starts-with? status "done") {:status "done" :effects (list {:verb "digest" :args (list id)})})
(else {:status "failed" :error body})))))))
;; a KERNEL runner — a seam runner that dispatches a durable behavior to the persistent kernel at
;; `base` (e.g. "http://sx_kernel:8930"), branching on the activity's category. Advertises {suspend},
;; so select-runner routes durable DAGs here.
(define host/ra--make-kernel-runner
(fn (base)
{:capabilities (list "effect" "branch" "each" "suspend")
:run (fn (dag env)
(host/ra--parse-kernel
(host/ra--http-get (str base "/flow/start/" (get (get env :activity) :category)))))}))
;; resume a suspended kernel instance (the async boundary) → the runner contract.
(define host/ra--kernel-resume
(fn (base id)
(host/ra--parse-kernel-resume (host/ra--http-get (str base "/flow/resume/" id)))))
(define host/ra--parse-kernel-resume
(fn (body)
(if (starts-with? body "resume:done")
{:status "done" :effects (list {:verb "digest-sent" :args (list)})}
{:status "failed" :error body})))