Summary
InMemoryBackend.Set stores the caller's map/slice reference directly, and Get returns it without copying, creating a data race between the caller and concurrent readers or writers.
Context
memory.go:389 does b.data[ck][key] = value — the caller's map[string]any pointer is stored directly in the backend. Get at line 402 returns that same pointer after releasing RLock. Any caller that reads a value, then modifies the returned map, races with another goroutine that is concurrently reading or writing the same key. SetVector has the same pattern with []float64 and metadata maps. The backend's RWMutex protects the map structure but does nothing to protect the values stored inside it.
Scope
In Scope
- Deep-copy
map[string]any values on Set (before storing) and on Get (before returning).
- Deep-copy
[]float64 slices and associated metadata maps in SetVector / GetVector.
- A JSON marshal/unmarshal round-trip is an acceptable deep-copy strategy if a dedicated recursive clone is not already available in the codebase.
Out of Scope
- Changing the
MemoryBackend interface signature (context propagation is tracked separately in G2).
- Optimising for zero-copy hot paths — correctness first.
- Fixing
ControlPlaneMemoryBackend — that is tracked separately.
Files
sdk/go/agent/memory.go:389 — deep-copy value in Set before storing
sdk/go/agent/memory.go:402 — deep-copy value in Get before returning
sdk/go/agent/memory.go:435 — deep-copy slice and metadata in SetVector / GetVector
sdk/go/agent/memory_test.go — race test: store a map, mutate the original, verify stored value is unchanged; store then mutate returned value, verify stored is unchanged; run with -race
Acceptance Criteria
Notes for Contributors
Severity: HIGH
Run go test -race ./sdk/go/agent/... before and after the fix to verify the race detector catches the existing bug and clears after the fix. A JSON-based deep copy (json.Marshal + json.Unmarshal) is safe and simple; a reflect-based recursive clone is faster but more code to maintain — choose based on the hot-path requirements of the memory backend.
Summary
InMemoryBackend.Setstores the caller's map/slice reference directly, andGetreturns it without copying, creating a data race between the caller and concurrent readers or writers.Context
memory.go:389doesb.data[ck][key] = value— the caller'smap[string]anypointer is stored directly in the backend.Getat line 402 returns that same pointer after releasingRLock. Any caller that reads a value, then modifies the returned map, races with another goroutine that is concurrently reading or writing the same key.SetVectorhas the same pattern with[]float64and metadata maps. The backend'sRWMutexprotects the map structure but does nothing to protect the values stored inside it.Scope
In Scope
map[string]anyvalues onSet(before storing) and onGet(before returning).[]float64slices and associated metadata maps inSetVector/GetVector.Out of Scope
MemoryBackendinterface signature (context propagation is tracked separately in G2).ControlPlaneMemoryBackend— that is tracked separately.Files
sdk/go/agent/memory.go:389— deep-copy value inSetbefore storingsdk/go/agent/memory.go:402— deep-copy value inGetbefore returningsdk/go/agent/memory.go:435— deep-copy slice and metadata inSetVector/GetVectorsdk/go/agent/memory_test.go— race test: store a map, mutate the original, verify stored value is unchanged; store then mutate returned value, verify stored is unchanged; run with-raceAcceptance Criteria
Setafter the call does not affect the stored valueGetdoes not affect the stored valueSetVector/GetVectorslices and metadata-raceflag (go test -race ./sdk/go/...)make lint)Notes for Contributors
Severity: HIGH
Run
go test -race ./sdk/go/agent/...before and after the fix to verify the race detector catches the existing bug and clears after the fix. A JSON-based deep copy (json.Marshal+json.Unmarshal) is safe and simple; a reflect-based recursive clone is faster but more code to maintain — choose based on the hot-path requirements of the memory backend.