Skip to content

A stored model answer is redacted, so it cannot be used to reproduce what the model actually said #209

Description

@thedancingdeveloper

Found while debugging a real failure, 2026-08-05, using the answers #190 now stores.

What happened

An rdpapp item failed with the SEARCH text does not occur in the file as whole lines. #190 stores the implementer's answer, so the obvious move is to diff the SEARCH text against the file. The stored answer contained:

serde_json::from_str(r#"{"password":"[redacted]"}"#).unwrap();
assert_eq!(input.password.as_deref(), Some("not-for-logs"));

The file really contains "not-for-logs" on both lines. [redacted] is this repository's own marker: redaction.PATTERNS's assignment rule matched "password":"not-for-logs"source code in a test fixture — and rewrote it on the way into the store.

Redaction is applied only in store.append and audit.append, never to prompts, so the model was given the real file and the answer it sent was almost certainly correct at that point. But the record no longer says so.

The defect

Two things #190 promised are in tension, and nothing says which won:

Today the second silently wins, and the first is quietly false. The stored answer is not the answer. It is a redacted rendering of it, and there is no way to tell from the record which parts were rewritten or how many.

That is worse than it sounds for exactly the case where the record is most wanted: an exact-match edit failure is a question about characters, and the record has had characters changed.

Why it bites here specifically

Credential-shaped text is ordinary in source code. password:, api_key=, token: appear in fixtures, tests, config parsers and documentation. The assignment pattern cannot distinguish a real secret from a test fixture asserting how secrets are handled — and rdpapp, a credential vault, is full of the latter.

So the redactor is most aggressive precisely on the codebase where the answers matter most.

What is wanted

Not weakening #186 — the store must still never carry a credential. Options, roughly in increasing cost:

  1. Say that it happened, per answer. answer_redacted already exists as a boolean. Make it a count, or record which patterns fired. A reader would then know the text is not verbatim, instead of reading a rewritten answer as if it were the model's. Cheapest, and removes the silent part of the failure, which is the worst part.
  2. Record a digest of the unredacted text alongside the redacted body. Anyone holding the original can then prove a match without the store ever holding the secret.
  3. Distinguish a value from an assertion about a value. Almost certainly not worth it — deciding whether "password":"x" in a Rust test is a real credential is exactly the judgement a pattern cannot make, and guessing wrong in the permissive direction is unrecoverable.

I would do (1) now and argue about (2).

Blind spots

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:storeSQLite event store and its projectionstype:taskUnit of implementation work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions