Summary
Embedding vectors are hand-serialized into a string literal ("[" + ",".join(str(f) ...) + "]") and bound as text with CAST(:vec AS vector), bypassing the native pgvector codec. The ORM column already uses pgvector.sqlalchemy.Vector, but no asyncpg codec is registered, which is why the manual path exists.
Evidence
sentinel/persistence/repositories.py:819 (set_embedding) and :897 (similar_resolved_incidents) — manual ",".join(str(f) for f in ...) literal.
sentinel/persistence/models.py:70 — column typed Vector, but no register_vector / set_type_codec anywhere.
(str(float) round-trips, so this isn't numerically lossy — but it bypasses the codec the project otherwise depends on and duplicates serialization logic across two call sites.)
Fix
Register the pgvector asyncpg codec (pgvector.asyncpg.register_vector) at connection setup and bind list[float] directly, removing the manual literal construction.
Summary
Embedding vectors are hand-serialized into a string literal (
"[" + ",".join(str(f) ...) + "]") and bound as text withCAST(:vec AS vector), bypassing the native pgvector codec. The ORM column already usespgvector.sqlalchemy.Vector, but no asyncpg codec is registered, which is why the manual path exists.Evidence
sentinel/persistence/repositories.py:819(set_embedding) and:897(similar_resolved_incidents) — manual",".join(str(f) for f in ...)literal.sentinel/persistence/models.py:70— column typedVector, but noregister_vector/set_type_codecanywhere.(
str(float)round-trips, so this isn't numerically lossy — but it bypasses the codec the project otherwise depends on and duplicates serialization logic across two call sites.)Fix
Register the pgvector asyncpg codec (
pgvector.asyncpg.register_vector) at connection setup and bindlist[float]directly, removing the manual literal construction.