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
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