diff --git a/_build/dev/lib/symbiont/.mix/compile.elixir b/_build/dev/lib/symbiont/.mix/compile.elixir index d9a2c6a..9973c9e 100644 Binary files a/_build/dev/lib/symbiont/.mix/compile.elixir and b/_build/dev/lib/symbiont/.mix/compile.elixir differ diff --git a/_build/prod/lib/symbiont/.mix/compile.elixir b/_build/prod/lib/symbiont/.mix/compile.elixir index ac458b0..fb6efbe 100644 Binary files a/_build/prod/lib/symbiont/.mix/compile.elixir and b/_build/prod/lib/symbiont/.mix/compile.elixir differ diff --git a/lib/symbiont/engram.ex b/lib/symbiont/engram.ex index 3e58518..706e21c 100644 --- a/lib/symbiont/engram.ex +++ b/lib/symbiont/engram.ex @@ -503,27 +503,41 @@ defmodule Symbiont.Engram do end defp generate_session_id do - now = NaiveDateTime.local_now() + now = DateTime.utc_now() hex = :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower) now - |> NaiveDateTime.to_iso8601() + |> DateTime.to_iso8601() |> String.replace(~r/[T:\-]/, "") |> String.slice(0, 14) |> Kernel.<>("-#{hex}") end defp now_iso do - NaiveDateTime.local_now() |> NaiveDateTime.to_iso8601() + DateTime.utc_now() |> DateTime.to_iso8601() end defp is_stale?(last_heartbeat) do - case NaiveDateTime.from_iso8601(last_heartbeat) do - {:ok, dt} -> - NaiveDateTime.diff(NaiveDateTime.local_now(), dt, :minute) > @stale_threshold_minutes + # Parse timestamp — handles both UTC DateTime (new) and naive (legacy Python) + with {:error, _} <- parse_as_datetime(last_heartbeat), + {:error, _} <- parse_as_naive(last_heartbeat) do + false + else + {:ok, dt} -> DateTime.diff(DateTime.utc_now(), dt, :minute) > @stale_threshold_minutes + end + end - _ -> - false + defp parse_as_datetime(str) do + case DateTime.from_iso8601(str) do + {:ok, dt, _offset} -> {:ok, dt} + _ -> {:error, :invalid} + end + end + + defp parse_as_naive(str) do + case NaiveDateTime.from_iso8601(str) do + {:ok, ndt} -> {:ok, DateTime.from_naive!(ndt, "Etc/UTC")} + _ -> {:error, :invalid} end end @@ -546,49 +560,59 @@ defmodule Symbiont.Engram do defp exec(conn, sql, params \\ []) do {:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql) - if params != [] do - :ok = Exqlite.Sqlite3.bind(stmt, params) - end + try do + if params != [] do + :ok = Exqlite.Sqlite3.bind(stmt, params) + end - case Exqlite.Sqlite3.step(conn, stmt) do - :done -> :ok - {:row, _} -> :ok - {:error, reason} -> {:error, reason} + case Exqlite.Sqlite3.step(conn, stmt) do + :done -> :ok + {:row, _} -> :ok + {:error, reason} -> raise "SQLite exec error: #{inspect(reason)}" + end + after + Exqlite.Sqlite3.release(conn, stmt) end - after - :ok end defp query_one(conn, sql, params \\ []) do {:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql) - if params != [] do - :ok = Exqlite.Sqlite3.bind(stmt, params) - end + try do + if params != [] do + :ok = Exqlite.Sqlite3.bind(stmt, params) + end - case Exqlite.Sqlite3.step(conn, stmt) do - {:row, row} -> {:ok, row} - :done -> :empty - {:error, reason} -> {:error, reason} + case Exqlite.Sqlite3.step(conn, stmt) do + {:row, row} -> {:ok, row} + :done -> :empty + {:error, reason} -> raise "SQLite query error: #{inspect(reason)}" + end + after + Exqlite.Sqlite3.release(conn, stmt) end end defp query_all(conn, sql, params \\ []) do {:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql) - if params != [] do - :ok = Exqlite.Sqlite3.bind(stmt, params) - end + try do + if params != [] do + :ok = Exqlite.Sqlite3.bind(stmt, params) + end - rows = collect_rows(conn, stmt, []) - {:ok, rows} + rows = collect_rows(conn, stmt, []) + {:ok, rows} + after + Exqlite.Sqlite3.release(conn, stmt) + end end defp collect_rows(conn, stmt, acc) do case Exqlite.Sqlite3.step(conn, stmt) do {:row, row} -> collect_rows(conn, stmt, [row | acc]) :done -> Enum.reverse(acc) - {:error, _reason} -> Enum.reverse(acc) + {:error, reason} -> raise "SQLite step error mid-collection: #{inspect(reason)}" end end end