Skip to content

fix(memory): extract memories from sanitized response text when PII response sanitization is on - #16401

Merged
diegosouzapw merged 3 commits into
cheaper-inference:release/v3.8.52from
yourspraveen:fix/memory-extraction-pii-sanitization
Oct 13, 2026
Merged

diegosouzapw merged 3 commits into
cheaper-inference:release/v3.8.52from
yourspraveen:fix/memory-extraction-pii-sanitization

Conversation

@yourspraveen

Copy link
Copy Markdown
Contributor

⚠️ base-red inherited: #15306

Closes #16400

Problem

With PII_RESPONSE_SANITIZATION on, the client receives redacted output, but persistent memory extracts facts from the unsanitized response. Both completion paths call runMemoryExtractionGate before response sanitization runs:

  • Non-streaming: nonStreamingResponse.ts extracts from translatedResponse at ~L715. piiMasker.postCall → sanitizePIIResponse runs later, in the post-call guardrails at ~L761.
  • Streaming: streamingTail.ts (~L556) extracts from streamResponseBody. That body is assembled in the provider transform, upstream of the PII SSE transform (streamingPipeline.ts ~L89–99).

So raw emails, SSNs and similar values are written to durable memory and can be re-injected into later prompts.

Fix

runMemoryExtractionGate now runs the extracted response text through sanitizePII, the same sanitizer and mode handling the response path uses (sanitizePIIResponse applies sanitizePII to each string).

  • One place: the gate is shared, so both paths are fixed with a single change.
  • Flag off: no-op, so PII handling stays opt-in (Hard Rule feat: v0.2.0 — advanced routing services, cost analytics, pricing overhaul #20). sanitizePII returns the text unchanged.
  • No mutation: it sanitizes a copy of the text, never the response object. On the non-streaming path that object is the one later returned to the client and passed to the post-call guardrails. (sanitizePIIResponse would mutate in place, which is why it isn't used here.)
  • Request text: unchanged. Request-side masking is PII_REDACTION_ENABLED's job, and piiMasker.preCall has already rewritten the body before extraction.

I considered moving extraction after the post-call guardrails instead. That would fix only the non-streaming path, and it would mean editing frozen chat-core files.

Tests

tests/unit/memory-extraction-pii-sanitization.test.ts (6 tests):

  • Control: the flag is on in the test environment, and sanitizePII redacts the sample reply, so failures can't come from a misconfigured flag.
  • Flag on, chat completion: memory gets [EMAIL_REDACTED] and no raw email or SSN.
  • Flag on, Responses output_text: no raw email in memory.
  • Flag on: the response object passed to the gate is not mutated.
  • Flag off: memory text passes through unchanged.
  • Response flag: does not rewrite request-derived memory text.

Failing first: commit de12cb1afa adds the test alone. On the base, the two "flag on" extraction cases fail with the raw email and SSN in the extracted text. The other four pass. With the fix, all 6 pass.

The test is added to stryker.conf.json tap.testFiles, because memoryExtraction.ts is a mutated module.

Other test results:

  • Existing memory suites (node): 58/58 pass, including video-bridge-memory-suppression, chatcore-memory-extraction, memory-extraction and memory-extraction-boundary-truncation.
  • Vitest src/lib/memory: 91/91 pass.
  • Suites touching memoryExtraction, piiSanitizer, streamingTail or nonStreamingResponse: 287 of 292 pass with 1 failure. That failure is integration-wiring.test.ts › "keeps usage quota wired through A2A and MCP tools", which fails identically on the untouched base.

Local quality gates

I ran the quality.yml fast-gate set locally against release/v3.8.52:

  • Passing: file-size, complexity-ratchets, dead-code, duplication, error-helper, any-budget, cycles (ratchet), type-coverage, open-sse-typecheck, typecheck:core, ts7-diagnostics-ratchet, changelog-integrity, route-guard-membership, test-discovery, codeql-ratchet, ai-attribution and the rest. ESLint has 0 warnings on the changed files.

  • Failing, not caused by this change:

    • dashboard-typecheck: TS2339 in dashboard/providers/[id]/components/ConnectionRow.tsx.
    • agent-skills-sync: 20 skills to regenerate.
    • mutation-test-coverage: lists covering tests missing for accountFallback, routeGuard, utils/error and publicCreds. memoryExtraction.ts is no longer listed.

    All three fail identically on the untouched base.

  • Not run locally: workflows needs actionlint and zizmor. secrets, vuln-ratchet and openapi-breaking skip gracefully without their binaries.

…ation is on

runMemoryExtractionGate receives the response before PII response
sanitization on both paths (non-streaming: ahead of the post-call
guardrails; streaming: the body assembled upstream of the PII SSE
transform). With PII_RESPONSE_SANITIZATION on, the client sees
[EMAIL_REDACTED] while the raw address is extracted into memory.

Fails on the base: the two 'memory gets the redacted reply' cases.
The control and the flag-off / no-mutation / request-text guards pass.
…esponse sanitization is on

runMemoryExtractionGate is fed the response before PII response
sanitization on both completion paths, so with PII_RESPONSE_SANITIZATION
on the client saw [EMAIL_REDACTED] while the raw value was extracted into
durable memory. The gate now runs the extracted response text through
sanitizePII, the same sanitizer the response path uses. It is a no-op
while the flag is off (Hard Rule cheaper-inference#20) and works on the extracted text, so
the caller's response object is not mutated. Request-derived text is left
to PII_REDACTION_ENABLED, which has already masked the request body.

Adds the new test to stryker tap.testFiles (memoryExtraction.ts is a
mutated module).
@diegosouzapw
diegosouzapw merged commit 3160851 into cheaper-inference:release/v3.8.52 Oct 13, 2026
31 of 51 checks passed
@yourspraveen
yourspraveen deleted the fix/memory-extraction-pii-sanitization branch October 13, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(backend): Memory stores raw PII from model output when PII_RESPONSE_SANITIZATION is on

2 participants