Fix statement leaks, error handling, UTC timestamps in Engram

This commit is contained in:
Claude Opus 4.6 2026-03-23 12:32:09 +00:00
parent 1ee2976765
commit 337ce5e9f8
3 changed files with 54 additions and 30 deletions

View File

@ -503,27 +503,41 @@ defmodule Symbiont.Engram do
end end
defp generate_session_id do defp generate_session_id do
now = NaiveDateTime.local_now() now = DateTime.utc_now()
hex = :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower) hex = :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
now now
|> NaiveDateTime.to_iso8601() |> DateTime.to_iso8601()
|> String.replace(~r/[T:\-]/, "") |> String.replace(~r/[T:\-]/, "")
|> String.slice(0, 14) |> String.slice(0, 14)
|> Kernel.<>("-#{hex}") |> Kernel.<>("-#{hex}")
end end
defp now_iso do defp now_iso do
NaiveDateTime.local_now() |> NaiveDateTime.to_iso8601() DateTime.utc_now() |> DateTime.to_iso8601()
end end
defp is_stale?(last_heartbeat) do defp is_stale?(last_heartbeat) do
case NaiveDateTime.from_iso8601(last_heartbeat) do # Parse timestamp — handles both UTC DateTime (new) and naive (legacy Python)
{:ok, dt} -> with {:error, _} <- parse_as_datetime(last_heartbeat),
NaiveDateTime.diff(NaiveDateTime.local_now(), dt, :minute) > @stale_threshold_minutes {:error, _} <- parse_as_naive(last_heartbeat) do
false
else
{:ok, dt} -> DateTime.diff(DateTime.utc_now(), dt, :minute) > @stale_threshold_minutes
end
end
_ -> defp parse_as_datetime(str) do
false 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
end end
@ -546,49 +560,59 @@ defmodule Symbiont.Engram do
defp exec(conn, sql, params \\ []) do defp exec(conn, sql, params \\ []) do
{:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql) {:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql)
if params != [] do try do
:ok = Exqlite.Sqlite3.bind(stmt, params) if params != [] do
end :ok = Exqlite.Sqlite3.bind(stmt, params)
end
case Exqlite.Sqlite3.step(conn, stmt) do case Exqlite.Sqlite3.step(conn, stmt) do
:done -> :ok :done -> :ok
{:row, _} -> :ok {:row, _} -> :ok
{:error, reason} -> {:error, reason} {:error, reason} -> raise "SQLite exec error: #{inspect(reason)}"
end
after
Exqlite.Sqlite3.release(conn, stmt)
end end
after
:ok
end end
defp query_one(conn, sql, params \\ []) do defp query_one(conn, sql, params \\ []) do
{:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql) {:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql)
if params != [] do try do
:ok = Exqlite.Sqlite3.bind(stmt, params) if params != [] do
end :ok = Exqlite.Sqlite3.bind(stmt, params)
end
case Exqlite.Sqlite3.step(conn, stmt) do case Exqlite.Sqlite3.step(conn, stmt) do
{:row, row} -> {:ok, row} {:row, row} -> {:ok, row}
:done -> :empty :done -> :empty
{:error, reason} -> {:error, reason} {:error, reason} -> raise "SQLite query error: #{inspect(reason)}"
end
after
Exqlite.Sqlite3.release(conn, stmt)
end end
end end
defp query_all(conn, sql, params \\ []) do defp query_all(conn, sql, params \\ []) do
{:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql) {:ok, stmt} = Exqlite.Sqlite3.prepare(conn, sql)
if params != [] do try do
:ok = Exqlite.Sqlite3.bind(stmt, params) if params != [] do
end :ok = Exqlite.Sqlite3.bind(stmt, params)
end
rows = collect_rows(conn, stmt, []) rows = collect_rows(conn, stmt, [])
{:ok, rows} {:ok, rows}
after
Exqlite.Sqlite3.release(conn, stmt)
end
end end
defp collect_rows(conn, stmt, acc) do defp collect_rows(conn, stmt, acc) do
case Exqlite.Sqlite3.step(conn, stmt) do case Exqlite.Sqlite3.step(conn, stmt) do
{:row, row} -> collect_rows(conn, stmt, [row | acc]) {:row, row} -> collect_rows(conn, stmt, [row | acc])
:done -> Enum.reverse(acc) :done -> Enum.reverse(acc)
{:error, _reason} -> Enum.reverse(acc) {:error, reason} -> raise "SQLite step error mid-collection: #{inspect(reason)}"
end end
end end
end end