spike: PERSISTENT next/ kernel is viable — unblocks RA-live + TA-live

The shared prerequisite for both live steps was: does a next/ kernel process hold gen_server state
(flow_store) across HTTP requests? Confirmed yes. plans/ra_kernel.erl is a minimal kernel
(flow_store + register the publish-digest flow, then a blocking http:listen that keeps the
er-scheduler + gen_server alive); plans/ra-kernel-spike.sh boots it as a background sx_server and
drives it with two SEPARATE curls: GET /start suspends instance 1, GET /resume resumes that SAME
live instance → done. So durable suspend→resume across requests works on a persistent kernel.

Design decision (per the discussion): chose the persistent-kernel path (B) over host-side replay-log
(A). B serves BOTH durability (RA) and federation (TA) on one fed-sx-native substrate and exposes the
full next/ kernel (projections, outbox, actor model); A only solves flow durability and mixes Erlang
into the host process. The er-scheduler-context bug (which kills an in-process kernel, option C) does
NOT bite a separate-process kernel — er-bif-http-listen spawns each handler in-scheduler, so
gen_server:call completes. Gotchas recorded: a blocking listener hangs any in-process
erlang-eval-ast (the kernel must be a dedicated TCP-driven process), and binary =:= is buggy (always
true) so routes must pattern-match paths as byte-list binaries.

RA-live + TA-live are now BUILD work (a real kernel service + the host as HTTP client + the actor
model), not research — the prerequisite is proven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 17:00:32 +00:00
parent 39e5f906f2
commit 836d32474f
3 changed files with 132 additions and 5 deletions

41
plans/ra_kernel.erl Normal file
View File

@@ -0,0 +1,41 @@
%% plans/ra_kernel.erl — RA/TA persistent-kernel SPIKE.
%% A minimal long-lived next/ kernel: starts flow_store + registers the publish-digest flow, then
%% blocks in http:listen — keeping the er-scheduler (and flow_store's gen_server) alive across
%% requests. Two routes drive flow_store over HTTP: GET /start (start a newsletter flow → suspend)
%% and GET /resume (resume instance 1 → done). If /resume completes in a SEPARATE request from
%% /start, the gen_server persisted across requests — the persistent-kernel prerequisite for
%% RA-live + TA-live holds.
-module(ra_kernel).
-export([start/1]).
start(Port) ->
flow_store:start_link(),
FF = fun (_) -> [f1, f2, f3] end,
flow_store:register_flow(bd, blog_publish_digest:build([{fetch_followers, FF}])),
http:listen(Port, fun (Req) -> route(Req) end).
route(Req) ->
[{status, 200}, {headers, []}, {body, respond(field(path, Req))}].
%% /start (bytes 47,115,116,97,114,116)
respond(<<47,115,116,97,114,116>>) ->
Env = [{activity, [{type, create}, {actor, alice}, {id, <<110,49>>},
{object, [{type, article}, {category, newsletter}]}]},
{actor, alice}],
case flow_store:start(bd, Env) of
{ok, _Id, {flow_suspended, _}} -> <<"start:suspended">>;
{ok, _Id, {flow_done, _}} -> <<"start:done">>;
_ -> <<"start:other">>
end;
%% /resume (bytes 47,114,101,115,117,109,101)
respond(<<47,114,101,115,117,109,101>>) ->
case flow_store:resume(1, morning_ts) of
{ok, {flow_done, _}} -> <<"resume:done">>;
{flow_done, _} -> <<"resume:done">>;
_ -> <<"resume:other">>
end;
respond(_) -> <<"path:unknown">>.
field(K, [{K, V} | _]) -> V;
field(K, [_ | Rest]) -> field(K, Rest);
field(_, []) -> nil.