Keep the latest Guardian risk score during concurrent sampling (#38580)

## Why

Concurrent Guardian samples can finish out of order, allowing an older sample to
replace the thread's newer in-memory security risk score.

## What changed

- Record when each `SecurityRiskScore` sample starts while preserving compatibility
  with records that have no timestamp.
- Add `ExtensionData::insert_if` so checking and replacing a typed attachment happen
  under the same lock.
- Update Guardian V2 to replace the thread's risk score only when the completed sample
  is newer than the stored one.

## Testing

- Verify concurrent conditional inserts retain the newest value.
- Verify Guardian-generated risk scores include a sampling timestamp.

GitOrigin-RevId: 6543a4e5497da4c623331050789e91613f931c4b
This commit is contained in:
jif
2026-08-14 13:44:51 +00:00
committed by copyberry
parent 42d842a91a
commit 2a452d7dc1
11 changed files with 76 additions and 2 deletions
@@ -204,6 +204,7 @@ fn ignores_legacy_abort_without_turn_id_and_context_only_records() {
}));
let security_risk = project(RolloutItem::SecurityRiskScore(SecurityRiskScore {
scores: BTreeMap::from([("action_risk".to_string(), 0.92)]),
sampled_at: None,
}));
assert!(aborted.is_empty());