feat(daemon): own startup defaults and readiness
Give standalone and local launches the same defaults, protected discovery record, and honest health status.
11 unmodified lines12131415161617181920214 unmodified lines11 unmodified lines
.DS_Store
/native/render/target//priv/bin//priv/bin/*!/priv/bin/albedo-daemon/cli/bin/albedo
/.direnv/.envrc4 unmodified lines128 unmodified linesExpand all12913013113213313413313413513613713813922 unmodified lines128 unmodified linesExpand all cp -R build/erlang-shipment/. $out/lib/albedo/ mkdir -p $out/lib/albedo/albedo/priv/bin ln -s ${render}/bin/albedo-render $out/lib/albedo/albedo/priv/bin/albedo-render ln -s ${usageCore}/bin/usage $out/lib/albedo/albedo/priv/bin/usage makeWrapper $out/lib/albedo/entrypoint.sh $out/bin/albedo-daemon \ --add-flags run \ makeWrapper $out/lib/albedo/albedo/priv/bin/albedo-daemon $out/bin/albedo-daemon \ --add-flags "$out/lib/albedo/entrypoint.sh run" \ --set-default ALBEDO_BUILD $out/bin/albedo-daemon \ --prefix PATH : ${lib.makeBinPath [erlang python311 bash coreutils render]} runHook postInstall ''; };22 unmodified lines22 unmodified lines23242526272728293022 unmodified lines fidonecat >"$out/albedo-daemon" <<EOF#!/bin/shexec erl -pa "$out"/*/ebin -eval 'albedo@@main:run(albedo)' -noshell -extraexec "$root/priv/bin/albedo-daemon" erl -pa "$out"/*/ebin -eval 'albedo@@main:run(albedo)' -noshell -extraEOFchmod +x "$out/albedo-daemon"echo "$out/albedo-daemon"123456789101112#!/bin/sh# VM defaults must be installed before Erlang starts in every launch mode.set -euvm_defaults='+P 65536 +Q 16384 +MBsbct 16 +MHsbct 32 +MBlmbcs 256 +MHlmbcs 256 +MBsmbcs 32 +MHsmbcs 32 +MBas aobf +MHas aobf +MBacul 0 +MHacul 0'ERL_FLAGS="$vm_defaults ${ERL_FLAGS:-}"export ERL_FLAGSif [ "$#" -eq 0 ]; then source_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) cd "$source_root" exec gleam runfiexec "$@"1234567813 unmodified lines22232425252627282930313217 unmodified lines/// Concurrent ownership of one home conflicts with the one-daemon E2E harness.import albedo/daemon/serverimport gleam/erlang/processimport gleam/resultimport gleeunit/should
@external(erlang, "albedo_skills_test_support", "fixture")fn fixture() -> #(String, String, String)13 unmodified lines }) let assert Ok(#(first, release)) = process.receive(claimed, 5000) first |> should.equal(Ok(Nil))
server.claim_home(home) |> should.equal(Error(Nil)) server.claim_home(home) |> result.map_error(fn(_) { Nil }) |> should.equal(Error(Nil))
// The connection closes when the owner's resources are freed, which can land // just after its exit is observable, so a fresh claimant retries briefly. process.send(release, Nil)17 unmodified lines12234567891011121314151617181920212223242526272829303115 unmodified lines474849502751525354555657585960616274 unmodified lines-module(albedo_daemon).-export([env/1,free_port/0,ready/3,read_config/1,directory/1,shutdown/0,rss/1,watch_parent/1,hold/1,http_request/1]).-export([defaults/0,refuse_home/1,env/1,free_port/0,ready/3,read_config/1,directory/1,shutdown/0,rss/1,watch_parent/1,hold/1,http_request/1]).env(Name) -> case os:getenv(binary_to_list(Name)) of false -> <<>>; Value -> unicode:characters_to_binary(Value) end.%% Home and authentication belong to the daemon, including direct starts.defaults() -> try Home = case env(<<"ALBEDO_HOME">>) of <<>> -> filename:join(unicode:characters_to_binary(os:getenv("HOME")), <<".albedo">>); Specified -> Specified end, Absolute = filename:absname(Home), ok = filelib:ensure_dir(filename:join(Absolute, <<"daemon.lock">>)), ok = file:change_mode(Absolute, 8#700), Token = case env(<<"ALBEDO_TOKEN">>) of <<>> -> binary:encode_hex(crypto:strong_rand_bytes(32), lowercase); Supplied when byte_size(Supplied) >= 32 -> Supplied; _ -> error(token_too_short) end, {ok, {Absolute, Token}} catch Class:Reason -> {error, unicode:characters_to_binary(io_lib:format("daemon defaults failed: ~p:~p", [Class, Reason]))} end.
refuse_home(Home) -> io:format(standard_error, "storage is in use by another albedo daemon or maintenance command for ALBEDO_HOME=~ts~n", [Home]), erlang:halt(75).
%% A loopback port nothing listens on, which the OS just picked.free_port() -> {ok, Socket} = gen_tcp:listen(0, [{ip, loopback}, {reuseaddr, true}]), {ok, Port} = inet:port(Socket),15 unmodified lines false -> Record; Build -> Record#{build=>unicode:characters_to_binary(Build)} end), case file:write_file(Temp,Data,[write,sync]) of ok -> ok=file:change_mode(Temp,8#600), file:rename(Temp,File), {ok,nil}; ok -> case file:change_mode(Temp,8#600) of ok -> case file:rename(Temp,File) of ok -> {ok,nil}; {error,Reason} -> {error,atom_to_binary(Reason)} end; {error,Reason} -> {error,atom_to_binary(Reason)} end; {error,Reason} -> {error,atom_to_binary(Reason)} end.%% Called once the drain has closed every session, kernel and store. What%% init:stop() would still do is take the VM down application by application,74 unmodified lines137 unmodified linesExpand all138139140141142143144145146106 unmodified linesExpand all253254255256257258259260261262263264293 unmodified linesExpand all5585595605615625635645655665675685695705715725731402 unmodified linesExpand all1976197719781979196719801981198219831984198519861987198819891990214 unmodified linesExpand all22052206220722082209221022112212221322142215221622172218221922202221222222232224222521902191219221932194219521962197219821992200220122022203220422052206220722082209221022262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248221222132214224922502251225222532254225522562257225822592260636 unmodified linesExpand all289728982899290028592860290129022903290429051 unmodified line29072908290929102870287128722873291129122913291429152916291729182919292020 unmodified lines2941294229432944290229032904290529452946294729482949295029512952291229532954291429552956295729582959315 unmodified linesExpand all3275327632773278327932803281328232833284137 unmodified linesExpand all /// was not running, or a recipient that refused it while busy has come to /// rest. MailWaiting Shutdown ReadHealth(Subject(Result(Nil, String)))}
type State { State(106 unmodified linesExpand all}
fn serve(state: State, message: Message) -> actor.Next(State, a) { case message { ReadHealth(reply) -> { process.send(reply, Ok(Nil)) actor.continue(state) } CreateOperation(operation, cwd, provider, model, reply) -> { let #(state, outcome) = create_operation(state, operation, cwd, provider, model) process.send(reply, outcome)293 unmodified linesExpand all
/// A registry that is not open answers without the store, which may be gone.fn refuse(state: State, message: Message) -> Nil { case message { ReadHealth(reply) -> process.send( reply, Error(case state.stage { Closing -> "daemon_stopping" _ -> "daemon_unavailable" }), ) Create(_, _, _, reply) -> process.send(reply, Error(closed)) CreateOperation(_, _, _, _, reply) | SubmitOperation(_, _, reply) -> process.send(reply, Error(closed)) CreateChild(_, _, _, _, reply) -> process.send(reply, Error(closed))1402 unmodified linesExpand all) -> response.Response(mist.ResponseData) { case http_request(fn() { handle_request(config, registry, req) }) { Ok(response) -> response Error(_) -> error(503, "daemon request process is unavailable") reply( 503, json.object([ #("code", json.string("daemon_unavailable")), #("error", json.string("daemon request process is unavailable")), ]), ) |> response.set_header("connection", "close") }}
214 unmodified linesExpand all live: request.Request(mist.Connection),) -> response.Response(mist.ResponseData) { case req.method, request.path_segments(req) { Get, ["health"] -> case actor.call(registry, 5000, ReadHealth) { Error(code) -> reply( 503, json.object([ #("code", json.string(code)), #( "error", json.string(case code { "daemon_stopping" -> "daemon is stopping" _ -> "daemon is unavailable" }), ), ]), ) Ok(_) -> { let fields = [ reply( 200, acknowledged([ #("version", json.int(2)), #( "capabilities", json.array( [ "submission_cancellation", "session_provider", "session_workspace", "session_extensions", "global_extensions", "session_tree", "session_context", "session_commands", "workspace_browser", "settings_api", "session_model", ], json.string, #("version", json.int(2)), #( "capabilities", json.array( [ "operation_receipts", "session_stream_generation", "agents_stream_overflow", "submission_cancellation", "session_provider", "session_workspace", "session_extensions", "global_extensions", "session_tree", "session_context", "session_commands", "workspace_browser", "settings_api", "session_model", ], json.string, ), ), ), ]), ) ] let fields = case env("ALBEDO_BUILD") { "" -> fields build -> [#("build", json.string(build)), ..fields] } reply(200, acknowledged(fields)) } } Get, ["settings"] -> settings.snapshot(config.home) |> answered_settings Put, ["settings", "providers", name] -> body(req, decode.dynamic) |> result.try(fn(value) {636 unmodified linesExpand all )}
pub fn main() -> Nil { let home = env("ALBEDO_HOME") let token = env("ALBEDO_TOKEN") let assert Ok(#(home, token)) = defaults() let config = Config( home, token,1 unmodified line setting("ALBEDO_KERNEL_BUDGET_MB", 2048, 64, 1_048_576) * 1024, setting("ALBEDO_STATE_EXPIRY_SECONDS", 1_209_600, 1, 31_536_000), setting("ALBEDO_SCHEDULE_TICK_MS", 15_000, 50, 60_000), ) let assert True = string.byte_size(token) >= 32 && home != "" as "start albedo through its CLI" let assert Ok(_) = claim_home(home) as "storage is in use by another albedo daemon or maintenance command for this ALBEDO_HOME" case claim_home(home) { Ok(_) -> Nil Error(sqlight.SqlightError(code: sqlight.Busy, ..)) | Error(sqlight.SqlightError(code: sqlight.Locked, ..)) -> refuse_home(home) Error(error) -> panic as error.message } // A fixed port gives services such as the proxy a stable base url. let assert Ok(port) = start(config, setting("ALBEDO_PORT", 0, 0, 65_535)) let assert Ok(_) = ready(home, port, token) inspect(home)20 unmodified lines/// would run every in-flight turn twice. Offline cleanup takes the same lock/// before revalidation and holds it through deletion and vacuum. Neither owner/// commits the transaction or replaces the lock file. The OS drops the lock/// when its owner exits, so a crashed owner never leaves it stale.pub fn claim_home(home: String) -> Result(Nil, Nil) { use connection <- result.try( sqlight.open(home <> "/daemon.lock") |> result.replace_error(Nil), )pub fn claim_home(home: String) -> Result(Nil, sqlight.Error) { use connection <- result.try(sqlight.open(home <> "/daemon.lock")) case // The transaction is never committed: holding it open keeps the exclusive // lock, and a refused BEGIN leaves no lock behind on its connection. sqlight.exec("BEGIN EXCLUSIVE;", connection) { Ok(_) -> Ok(hold(connection)) Error(_) -> { Error(error) -> { let _ = sqlight.close(connection) Error(Nil) Error(error) } }}
315 unmodified linesExpand all <> json.to_string(json.string(operation_id)) <> "," <> string.drop_start(encoded, 1)}
@external(erlang, "albedo_daemon", "defaults")fn defaults() -> Result(#(String, String), String)
@external(erlang, "albedo_daemon", "refuse_home")fn refuse_home(home: String) -> Nil