-module(pig@obs@session). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/pig/obs/session.gleam"). -export([iso_timestamp/0, format_event/1, start/1, stop/1, record/2, record_sync/2, decode_tool_call/0, decode_message/0, replay/1, start_consumer/1, supervised/2]). -export_type([session_writer/0, writer_message/0, state/0, replay_error/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " JSONL session writer — records SessionEvents to a file for replay.\n" "\n" " OTP actor that receives SessionEvents and appends them as JSONL lines.\n" " Two modes:\n" " - `record()` — fire-and-forget, never blocks the agent.\n" " - `record_sync()` — synchronous call, blocks until written. For testing.\n" ). -opaque session_writer() :: {session_writer, gleam@erlang@process:subject(writer_message())}. -type writer_message() :: {write_event, pig@obs@events:session_event()} | {write_event_sync, pig@obs@events:session_event(), gleam@erlang@process:subject(nil)} | stop. -type state() :: {state, binary()}. -type replay_error() :: {file_error, binary()} | {parse_error, binary()}. -file("src/pig/obs/session.gleam", 40). ?DOC(" Get current ISO 8601 timestamp.\n"). -spec iso_timestamp() -> binary(). iso_timestamp() -> pig_obs_session_ffi:iso_timestamp(). -file("src/pig/obs/session.gleam", 618). -spec error_to_json(pig_protocol@error:ai_error()) -> gleam@json:json(). error_to_json(Error) -> case Error of {api_error, Message} -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"api_error"/utf8>>)}, {<<"message"/utf8>>, gleam@json:string(Message)}] ); rate_limited -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"rate_limited"/utf8>>)}] ); timeout -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"timeout"/utf8>>)}] ); {invalid_response, Detail} -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"invalid_response"/utf8>>)}, {<<"detail"/utf8>>, gleam@json:string(Detail)}] ) end. -file("src/pig/obs/session.gleam", 641). -spec reason_to_json(pig@obs@events:session_end_reason()) -> gleam@json:json(). reason_to_json(Reason) -> case Reason of normal_end -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"normal_end"/utf8>>)}] ); {error_end, E} -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"error"/utf8>>)}, {<<"error"/utf8>>, error_to_json(E)}] ); {max_iterations_exceeded, N} -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"max_iterations_exceeded"/utf8>>)}, {<<"max_iterations"/utf8>>, gleam@json:int(N)}] ); interrupted -> gleam@json:object( [{<<"type"/utf8>>, gleam@json:string(<<"interrupted"/utf8>>)}] ) end. -file("src/pig/obs/session.gleam", 610). -spec tool_call_to_json(pig_protocol@message:tool_call()) -> gleam@json:json(). tool_call_to_json(Tc) -> gleam@json:object( [{<<"id"/utf8>>, gleam@json:string(erlang:element(2, Tc))}, {<<"name"/utf8>>, gleam@json:string(erlang:element(3, Tc))}, {<<"arguments"/utf8>>, gleam@json:string(erlang:element(4, Tc))}] ). -file("src/pig/obs/session.gleam", 558). -spec message_to_json(pig_protocol@message:message()) -> gleam@json:json(). message_to_json(Msg) -> case Msg of {user, Content} -> gleam@json:object( [{<<"role"/utf8>>, gleam@json:string(<<"user"/utf8>>)}, {<<"content"/utf8>>, gleam@json:string(Content)}] ); {system, Content@1} -> gleam@json:object( [{<<"role"/utf8>>, gleam@json:string(<<"system"/utf8>>)}, {<<"content"/utf8>>, gleam@json:string(Content@1)}] ); {assistant, Content@2, Tool_calls, Thinking, Stop_reason} -> Base_fields = [{<<"role"/utf8>>, gleam@json:string(<<"assistant"/utf8>>)}, {<<"content"/utf8>>, gleam@json:string(Content@2)}, {<<"tool_calls"/utf8>>, gleam@json:array(Tool_calls, fun tool_call_to_json/1)}], Fields_with_thinking = case Thinking of {some, T} -> case T of {thinking, Content@3} -> lists:append( Base_fields, [{<<"thinking"/utf8>>, gleam@json:object( [{<<"content"/utf8>>, gleam@json:string(Content@3)}] )}] ) end; none -> Base_fields end, Fields_with_stop_reason = case Stop_reason of {some, Sr} -> lists:append( Fields_with_thinking, [{<<"stop_reason"/utf8>>, pig_protocol@stop_reason:to_json(Sr)}] ); none -> Fields_with_thinking end, gleam@json:object(Fields_with_stop_reason); {tool, Tool_call_id, Content@4} -> gleam@json:object( [{<<"role"/utf8>>, gleam@json:string(<<"tool"/utf8>>)}, {<<"tool_call_id"/utf8>>, gleam@json:string(Tool_call_id)}, {<<"content"/utf8>>, gleam@json:string(Content@4)}] ) end. -file("src/pig/obs/session.gleam", 664). -spec hook_to_string(pig@obs@events:hook_point()) -> binary(). hook_to_string(Hook) -> case Hook of before_tool_call -> <<"before_tool_call"/utf8>>; after_tool_call -> <<"after_tool_call"/utf8>>; before_inference -> <<"before_inference"/utf8>>; after_inference -> <<"after_inference"/utf8>>; on_error -> <<"on_error"/utf8>>; on_complete -> <<"on_complete"/utf8>>; on_session_start -> <<"on_session_start"/utf8>>; on_session_shutdown -> <<"on_session_shutdown"/utf8>> end. -file("src/pig/obs/session.gleam", 350). ?DOC(" Format a SessionEvent as a JSON string (pure function, no side effects).\n"). -spec format_event(pig@obs@events:session_event()) -> binary(). format_event(Event) -> Ts = iso_timestamp(), case Event of {session_started, Agent_id, Agent_name, Model, Provider_name, System_prompt} -> Fields = [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"session_started"/utf8>>)}, {<<"model"/utf8>>, gleam@json:string(Model)}], With_agent_id = case Agent_id of {some, V} -> lists:append( Fields, [{<<"agent_id"/utf8>>, gleam@json:string(V)}] ); none -> Fields end, With_agent_name = case Agent_name of {some, V@1} -> lists:append( With_agent_id, [{<<"agent_name"/utf8>>, gleam@json:string(V@1)}] ); none -> With_agent_id end, With_provider = case Provider_name of {some, V@2} -> lists:append( With_agent_name, [{<<"provider_name"/utf8>>, gleam@json:string(V@2)}] ); none -> With_agent_name end, With_system = case System_prompt of {some, V@3} -> lists:append( With_provider, [{<<"system_prompt"/utf8>>, gleam@json:string(V@3)}] ); none -> With_provider end, _pipe = gleam@json:object(With_system), gleam@json:to_string(_pipe); {inference_started, Model@1, Message_count} -> _pipe@1 = gleam@json:object( [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"inference_started"/utf8>>)}, {<<"model"/utf8>>, gleam@json:string(Model@1)}, {<<"message_count"/utf8>>, gleam@json:int(Message_count)}] ), gleam@json:to_string(_pipe@1); {inference_completed, Message, Response_id, Response_model, Stop_reason, Input_tokens, Output_tokens, Duration_ms, Input_messages} -> Fields@1 = [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"inference_completed"/utf8>>)}, {<<"duration_ms"/utf8>>, gleam@json:int(Duration_ms)}, {<<"message"/utf8>>, message_to_json(Message)}, {<<"input_messages"/utf8>>, gleam@json:array(Input_messages, fun message_to_json/1)}], With_response_id = case Response_id of {some, V@4} -> lists:append( Fields@1, [{<<"response_id"/utf8>>, gleam@json:string(V@4)}] ); none -> Fields@1 end, With_response_model = case Response_model of {some, V@5} -> lists:append( With_response_id, [{<<"response_model"/utf8>>, gleam@json:string(V@5)}] ); none -> With_response_id end, With_stop_reason = case Stop_reason of {some, V@6} -> lists:append( With_response_model, [{<<"stop_reason"/utf8>>, pig_protocol@stop_reason:to_json(V@6)}] ); none -> With_response_model end, With_input_tokens = case Input_tokens of {some, V@7} -> lists:append( With_stop_reason, [{<<"input_tokens"/utf8>>, gleam@json:int(V@7)}] ); none -> With_stop_reason end, With_output_tokens = case Output_tokens of {some, V@8} -> lists:append( With_input_tokens, [{<<"output_tokens"/utf8>>, gleam@json:int(V@8)}] ); none -> With_input_tokens end, _pipe@2 = gleam@json:object(With_output_tokens), gleam@json:to_string(_pipe@2); {tool_started, Tool_call} -> _pipe@3 = gleam@json:object( [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"tool_started"/utf8>>)}, {<<"tool_call"/utf8>>, tool_call_to_json(Tool_call)}] ), gleam@json:to_string(_pipe@3); {tool_executed, Tool_call@1, Result, Duration_ms@1} -> _pipe@4 = gleam@json:object( [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"tool_executed"/utf8>>)}, {<<"duration_ms"/utf8>>, gleam@json:int(Duration_ms@1)}, {<<"tool_call"/utf8>>, tool_call_to_json(Tool_call@1)}, {<<"result"/utf8>>, gleam@json:string(Result)}] ), gleam@json:to_string(_pipe@4); {tool_blocked, Tool_call@2, Hook_name, Reason} -> _pipe@5 = gleam@json:object( [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"tool_blocked"/utf8>>)}, {<<"tool_call"/utf8>>, tool_call_to_json(Tool_call@2)}, {<<"hook_name"/utf8>>, gleam@json:string(Hook_name)}, {<<"reason"/utf8>>, gleam@json:string(Reason)}] ), gleam@json:to_string(_pipe@5); {hook_acted, Hook_name@1, Hook_point, Action} -> _pipe@6 = gleam@json:object( [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"hook_acted"/utf8>>)}, {<<"hook_name"/utf8>>, gleam@json:string(Hook_name@1)}, {<<"hook_point"/utf8>>, gleam@json:string(hook_to_string(Hook_point))}, {<<"action"/utf8>>, gleam@json:object( [{<<"action_type"/utf8>>, gleam@json:string(erlang:element(2, Action))}, {<<"description"/utf8>>, gleam@json:string(erlang:element(3, Action))}] )}] ), gleam@json:to_string(_pipe@6); {inference_failed, Error, Duration_ms@2, Input_messages@1} -> _pipe@7 = gleam@json:object( [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"inference_failed"/utf8>>)}, {<<"duration_ms"/utf8>>, gleam@json:int(Duration_ms@2)}, {<<"error"/utf8>>, error_to_json(Error)}, {<<"input_messages"/utf8>>, gleam@json:array( Input_messages@1, fun message_to_json/1 )}] ), gleam@json:to_string(_pipe@7); {session_ended, Reason@1} -> _pipe@8 = gleam@json:object( [{<<"ts"/utf8>>, gleam@json:string(Ts)}, {<<"event"/utf8>>, gleam@json:string(<<"session_ended"/utf8>>)}, {<<"reason"/utf8>>, reason_to_json(Reason@1)}] ), gleam@json:to_string(_pipe@8) end. -file("src/pig/obs/session.gleam", 517). -spec handle_message(state(), writer_message()) -> gleam@otp@actor:next(state(), writer_message()). handle_message(State, Message) -> case Message of {write_event, Event} -> Json_str = format_event(Event), _ = simplifile:append( erlang:element(2, State), <> ), gleam@otp@actor:continue(State); {write_event_sync, Event@1, Reply_subject} -> Json_str@1 = format_event(Event@1), _ = simplifile:append( erlang:element(2, State), <> ), gleam@erlang@process:send(Reply_subject, nil), gleam@otp@actor:continue(State); stop -> gleam@otp@actor:stop() end. -file("src/pig/obs/session.gleam", 66). ?DOC(" Start a new session writer actor that appends events to the given file.\n"). -spec start(binary()) -> {ok, session_writer()} | {error, gleam@otp@actor:start_error()}. start(Path) -> Builder = begin _pipe = gleam@otp@actor:new({state, Path}), gleam@otp@actor:on_message(_pipe, fun handle_message/2) end, case gleam@otp@actor:start(Builder) of {ok, Started} -> {ok, {session_writer, erlang:element(3, Started)}}; {error, E} -> {error, E} end. -file("src/pig/obs/session.gleam", 77). ?DOC(" Stop the session writer actor.\n"). -spec stop(session_writer()) -> nil. stop(Writer) -> {session_writer, Subject} = Writer, gleam@erlang@process:send(Subject, stop). -file("src/pig/obs/session.gleam", 83). ?DOC(" Record a session event. Fire-and-forget: does not block.\n"). -spec record(session_writer(), pig@obs@events:session_event()) -> nil. record(Writer, Event) -> {session_writer, Subject} = Writer, gleam@erlang@process:send(Subject, {write_event, Event}). -file("src/pig/obs/session.gleam", 90). ?DOC( " Record a session event synchronously. Blocks until the event is\n" " written to disk. Use this in tests for deterministic assertions.\n" ). -spec record_sync(session_writer(), pig@obs@events:session_event()) -> nil. record_sync(Writer, Event) -> {session_writer, Subject} = Writer, Reply_subject = gleam@erlang@process:new_subject(), gleam@erlang@process:send(Subject, {write_event_sync, Event, Reply_subject}), case gleam@erlang@process:'receive'(Reply_subject, 5000) of {ok, _} -> nil; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"pig/obs/session"/utf8>>, function => <<"record_sync"/utf8>>, line => 94, value => _assert_fail, start => 3452, 'end' => 3507, pattern_start => 3463, pattern_end => 3468}) end, nil. -file("src/pig/obs/session.gleam", 226). ?DOC(" Parse a Tool message from a ToolBlocked JSON line.\n"). -spec parse_blocked_tool_message(binary()) -> {ok, pig_protocol@message:message()} | {error, nil}. parse_blocked_tool_message(Line) -> Id_decoder = gleam@dynamic@decode:at( [<<"tool_call"/utf8>>, <<"id"/utf8>>], {decoder, fun gleam@dynamic@decode:decode_string/1} ), Hook_name_decoder = gleam@dynamic@decode:at( [<<"hook_name"/utf8>>], {decoder, fun gleam@dynamic@decode:decode_string/1} ), Reason_decoder = gleam@dynamic@decode:at( [<<"reason"/utf8>>], {decoder, fun gleam@dynamic@decode:decode_string/1} ), case gleam@json:parse(Line, Id_decoder) of {ok, Id} -> case {gleam@json:parse(Line, Hook_name_decoder), gleam@json:parse(Line, Reason_decoder)} of {{ok, Hook_name}, {ok, Reason}} -> {ok, {tool, Id, <<<<<<"Tool blocked by '"/utf8, Hook_name/binary>>/binary, "': "/utf8>>/binary, Reason/binary>>}}; {_, _} -> {error, nil} end; {error, _} -> {error, nil} end. -file("src/pig/obs/session.gleam", 210). ?DOC(" Parse a Tool message from a ToolExecuted JSON line.\n"). -spec parse_tool_message(binary()) -> {ok, pig_protocol@message:message()} | {error, nil}. parse_tool_message(Line) -> Id_decoder = gleam@dynamic@decode:at( [<<"tool_call"/utf8>>, <<"id"/utf8>>], {decoder, fun gleam@dynamic@decode:decode_string/1} ), Result_decoder = gleam@dynamic@decode:at( [<<"result"/utf8>>], {decoder, fun gleam@dynamic@decode:decode_string/1} ), case gleam@json:parse(Line, Id_decoder) of {ok, Id} -> case gleam@json:parse(Line, Result_decoder) of {ok, Result} -> {ok, {tool, Id, Result}}; {error, _} -> {error, nil} end; {error, _} -> {error, nil} end. -file("src/pig/obs/session.gleam", 182). ?DOC(" Decode the event type from a JSON line.\n"). -spec decode_event_type_str(binary()) -> binary(). decode_event_type_str(Line) -> Decoder = gleam@dynamic@decode:at( [<<"event"/utf8>>], {decoder, fun gleam@dynamic@decode:decode_string/1} ), case gleam@json:parse(Line, Decoder) of {ok, Event_type} -> Event_type; {error, _} -> <<""/utf8>> end. -file("src/pig/obs/session.gleam", 169). ?DOC(" Get lines after the given line.\n"). -spec lines_after(list(binary()), binary()) -> list(binary()). lines_after(Lines, Target) -> case Lines of [] -> []; [L | Rest] -> case L =:= Target of true -> Rest; false -> lines_after(Rest, Target) end end. -file("src/pig/obs/session.gleam", 302). ?DOC(" Decode Thinking from JSON.\n"). -spec decode_thinking() -> gleam@dynamic@decode:decoder(gleam@option:option(pig_protocol@message:thinking())). decode_thinking() -> gleam@dynamic@decode:field( <<"content"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Content) -> gleam@dynamic@decode:success({some, {thinking, Content}}) end ). -file("src/pig/obs/session.gleam", 308). ?DOC(" Decode a ToolCall from JSON.\n"). -spec decode_tool_call() -> gleam@dynamic@decode:decoder(pig_protocol@message:tool_call()). decode_tool_call() -> gleam@dynamic@decode:field( <<"id"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Id) -> gleam@dynamic@decode:field( <<"name"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Name) -> gleam@dynamic@decode:field( <<"arguments"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Arguments_json) -> gleam@dynamic@decode:success( {tool_call, Id, Name, Arguments_json} ) end ) end ) end ). -file("src/pig/obs/session.gleam", 251). ?DOC(" Decode a Message from JSON.\n"). -spec decode_message() -> gleam@dynamic@decode:decoder(pig_protocol@message:message()). decode_message() -> gleam@dynamic@decode:field( <<"role"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Role) -> case Role of <<"user"/utf8>> -> gleam@dynamic@decode:field( <<"content"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Content) -> gleam@dynamic@decode:success({user, Content}) end ); <<"system"/utf8>> -> gleam@dynamic@decode:field( <<"content"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Content@1) -> gleam@dynamic@decode:success({system, Content@1}) end ); <<"tool"/utf8>> -> gleam@dynamic@decode:field( <<"tool_call_id"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Tool_call_id) -> gleam@dynamic@decode:field( <<"content"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Content@2) -> gleam@dynamic@decode:success( {tool, Tool_call_id, Content@2} ) end ) end ); <<"assistant"/utf8>> -> gleam@dynamic@decode:field( <<"content"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Content@3) -> gleam@dynamic@decode:field( <<"tool_calls"/utf8>>, gleam@dynamic@decode:list(decode_tool_call()), fun(Tool_calls) -> gleam@dynamic@decode:optional_field( <<"thinking"/utf8>>, none, decode_thinking(), fun(Thinking) -> gleam@dynamic@decode:optional_field( <<"stop_reason"/utf8>>, none, gleam@dynamic@decode:optional( pig_protocol@stop_reason:decoder( ) ), fun(Stop_reason) -> gleam@dynamic@decode:success( {assistant, Content@3, Tool_calls, Thinking, Stop_reason} ) end ) end ) end ) end ); _ -> gleam@dynamic@decode:failure( {user, <<""/utf8>>}, <<"unknown message role: "/utf8, Role/binary>> ) end end ). -file("src/pig/obs/session.gleam", 191). ?DOC(" Parse input_messages and message from an InferenceCompleted JSON line.\n"). -spec parse_inference_messages(binary()) -> {ok, list(pig_protocol@message:message())} | {error, replay_error()}. parse_inference_messages(Line) -> Input_decoder = gleam@dynamic@decode:at( [<<"input_messages"/utf8>>], gleam@dynamic@decode:list(decode_message()) ), case gleam@json:parse(Line, Input_decoder) of {ok, Input_msgs} -> Msg_decoder = gleam@dynamic@decode:at( [<<"message"/utf8>>], decode_message() ), case gleam@json:parse(Line, Msg_decoder) of {ok, Response_msg} -> {ok, lists:append(Input_msgs, [Response_msg])}; {error, _} -> {error, {parse_error, <<"Failed to parse message: "/utf8, Line/binary>>}} end; {error, _} -> {error, {parse_error, <<"Failed to parse input_messages: "/utf8, Line/binary>>}} end. -file("src/pig/obs/session.gleam", 158). ?DOC(" Find the last InferenceCompleted line.\n"). -spec find_last_inference_completed(list(binary())) -> gleam@option:option(binary()). find_last_inference_completed(Lines) -> _pipe = Lines, gleam@list:fold( _pipe, none, fun(Acc, Line) -> case decode_event_type_str(Line) of <<"inference_completed"/utf8>> -> {some, Line}; _ -> Acc end end ). -file("src/pig/obs/session.gleam", 128). ?DOC(" Replay from a list of JSONL lines.\n"). -spec replay_lines(list(binary())) -> {ok, list(pig_protocol@message:message())} | {error, replay_error()}. replay_lines(Lines) -> Last_inference = find_last_inference_completed(Lines), case Last_inference of none -> {ok, []}; {some, Input_messages_json} -> case parse_inference_messages(Input_messages_json) of {ok, Messages} -> Remaining = lines_after(Lines, Input_messages_json), Tool_msgs = begin _pipe = Remaining, gleam@list:filter_map( _pipe, fun(Line) -> case decode_event_type_str(Line) of <<"tool_executed"/utf8>> -> parse_tool_message(Line); <<"tool_blocked"/utf8>> -> parse_blocked_tool_message(Line); _ -> {error, nil} end end ) end, {ok, lists:append(Messages, Tool_msgs)}; {error, E} -> {error, E} end end. -file("src/pig/obs/session.gleam", 111). ?DOC( " Replay a JSONL session file and reconstruct the message history.\n" "\n" " Strategy: find the last InferenceCompleted event. Its input_messages +\n" " message give the complete history. For partial sessions (crash mid-loop),\n" " also include Tool messages from ToolExecuted events after the last inference.\n" ). -spec replay(binary()) -> {ok, list(pig_protocol@message:message())} | {error, replay_error()}. replay(Path) -> case simplifile:read(Path) of {error, E} -> {error, {file_error, gleam@string:inspect(E)}}; {ok, Content} -> Lines = begin _pipe = Content, _pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>), gleam@list:filter(_pipe@1, fun(L) -> L /= <<""/utf8>> end) end, case Lines of [] -> {ok, []}; _ -> replay_lines(Lines) end end. -file("src/pig/obs/session.gleam", 541). ?DOC( " Handle consumer messages (SessionEvent directly, not wrapped in WriterMessage).\n" " Used by the supervised consumer actor that receives events from the dispatcher.\n" ). -spec handle_consumer_message(state(), pig@obs@events:session_event()) -> gleam@otp@actor:next(state(), pig@obs@events:session_event()). handle_consumer_message(State, Event) -> Json_str = format_event(Event), case simplifile:append( erlang:element(2, State), <> ) of {ok, _} -> gleam@otp@actor:continue(State); {error, _} -> gleam@otp@actor:stop() end. -file("src/pig/obs/session.gleam", 319). ?DOC( " Start a session consumer actor that accepts SessionEvent directly.\n" " Used by the dispatcher to fan out events. Returns the Subject for registration.\n" " This is the consumer version of the actor — it receives SessionEvent directly,\n" " not WriterMessage wrappers. Fire-and-forget: does not block.\n" ). -spec start_consumer(binary()) -> {ok, gleam@erlang@process:subject(pig@obs@events:session_event())} | {error, gleam@otp@actor:start_error()}. start_consumer(Path) -> Builder = begin _pipe = gleam@otp@actor:new({state, Path}), gleam@otp@actor:on_message(_pipe, fun handle_consumer_message/2) end, case gleam@otp@actor:start(Builder) of {ok, Started} -> {ok, erlang:element(3, Started)}; {error, E} -> {error, E} end. -file("src/pig/obs/session.gleam", 333). ?DOC( " Create a supervised session consumer actor for use in a supervision tree.\n" " The supervised actor's message type is SessionEvent directly (not WriterMessage).\n" ). -spec supervised( binary(), gleam@erlang@process:name(pig@obs@events:session_event()) ) -> gleam@otp@supervision:child_specification(nil). supervised(Path, Name) -> gleam@otp@supervision:worker( fun() -> Builder = begin _pipe = gleam@otp@actor:new({state, Path}), _pipe@1 = gleam@otp@actor:on_message( _pipe, fun handle_consumer_message/2 ), gleam@otp@actor:named(_pipe@1, Name) end, case gleam@otp@actor:start(Builder) of {ok, Started} -> {ok, {started, erlang:element(2, Started), nil}}; {error, E} -> {error, E} end end ).