fix(memory-plugin): URGENT — emit JSON null for nil metadata/propagation (closes #1794 prod regression) #1798

Merged
hongming merged 1 commits from fix/memory-plugin-nil-jsonb-marshal into main 2026-05-24 10:54:33 +00:00
2 changed files with 25 additions and 3 deletions
@@ -349,8 +349,23 @@ func scanMemory(row interface {
}
func marshalMetadata(m map[string]interface{}) ([]byte, error) {
// Returning Go nil here previously made lib/pq send the parameter
// as a bytea-typed NULL placeholder that PostgreSQL refused to
// implicitly cast into the jsonb column. The resulting INSERT
// failed with `pq: invalid input syntax for type json`, which
// surfaced first as "scan namespace" errors from the QueryRow
// path (the row never gets created so RETURNING produces no rows;
// the QueryRow error propagates through Scan). Caught 2026-05-24
// during the Phase A2 backfill — UpsertNamespace failed on every
// call, then the client's circuit breaker opened and ALL plugin
// writes started failing. The same hazard exists on the
// memory_records.propagation column (also marshalled via this
// helper). Sending the JSON literal `null` keeps the parameter
// typed as a valid jsonb value regardless of pq's auto-conversion
// heuristics — semantically identical to NULL for our consumers
// (they treat empty propagation/metadata as absent).
if m == nil {
return nil, nil
return []byte(`null`), nil
}
b, err := json.Marshal(m)
if err != nil {
@@ -17,12 +17,19 @@ import (
// --- marshalMetadata corner cases ---
func TestMarshalMetadata_Nil(t *testing.T) {
// Post-fix (2026-05-24): nil input must return the JSON literal
// `null` (4 bytes), NOT Go nil. Returning Go nil made lib/pq send
// a bytea NULL parameter that PostgreSQL refused to implicitly
// cast into the target jsonb column, breaking every UpsertNamespace
// and CommitMemory call with `pq: invalid input syntax for type
// json`. The Phase A2 backfill exposed it (production saw every
// POST /memories return 500 immediately post-tenant-recycle).
got, err := marshalMetadata(nil)
if err != nil {
t.Errorf("err = %v", err)
}
if got != nil {
t.Errorf("got = %v, want nil", got)
if string(got) != "null" {
t.Errorf("got = %q (%v), want %q", string(got), got, "null")
}
}