Skip to content

condense_json dedup in log_to_db skips fragment-less previous turns; inconsistent fragment marker keys #1530

Description

@itguruhaseeb

Following on from #760 (using condense_json() to de-duplicate prompt_json / response_json), I hit a few things in Response.log_to_db (llm/models.py) that look unintended. All three live in the same block:

replacements = {}
# Include replacements from previous responses
for previous_response in conversation.responses[:-1]:
    for fragment in (previous_response.prompt.fragments or []) + (
        previous_response.prompt.system_fragments or []
    ):
        fragment_id = ensure_fragment(db, fragment)
        replacements[f"f:{fragment_id}"] = fragment
        replacements[f"r:{previous_response.id}"] = (
            previous_response.text_or_raise()
        )

for i, fragment in enumerate(self.prompt.fragments):
    fragment_id = ensure_fragment(db, fragment)
    replacements[f"f{fragment_id}"] = fragment
    ...
for i, fragment in enumerate(self.prompt.system_fragments):
    fragment_id = ensure_fragment(db, fragment)
    replacements[f"f{fragment_id}"] = fragment
    ...

response_text = self.text_or_raise()
replacements[f"r:{response_id}"] = response_text

1. A previous turn's response text is only de-duplicated if that turn used fragments

replacements[f"r:{previous_response.id}"] = ... is set inside the for fragment in ... loop. If a previous response had no fragments, that inner loop never runs, so its text is never registered as a replacement, and it is stored verbatim in the next turn's prompt_json instead of being condensed to a {"$": "r:<id>"} marker.

This is the common case (most conversations don't use -f), so #760's de-duplication is skipped for exactly the everyday multi-turn path.

Repro (2-turn conversation, turn 1 has no fragments, turn 2 does):

turn-2 stored prompt_json:
{"messages": [
  {"role": "user", "content": "First question"},
  {"role": "assistant", "content": "ASSISTANT_TURN_ONE_UNIQUE_LONG_ANSWER_THAT_SHOULD_BE_DEDUPED_LATER"},
  {"role": "user", "content": {"$r": [{"$": "f1"}, "\nSecond question"]}}
]}

R1 assistant text de-duplicated (condensed to marker)?  False
Fragment text condensed?                                True

Turn 1's assistant text is stored in full; only turn 2's fragment is condensed. Moving the r:{previous_response.id} registration out of the for fragment loop (so it runs once per previous response) restores the dedup.

2. Inconsistent fragment marker keys

Previous-turn fragments are keyed f:{fragment_id} (with colon), while current-turn prompt and system fragments are keyed f{fragment_id} (no colon). The same fragment therefore gets two different marker ids depending on which turn is being logged. It doesn't corrupt anything today, but it's an easy inconsistency to trip over if/when the markers are ever read back.

3. Question: are the raw markers in llm logs --json intended?

llm logs --json returns the condensed forms directly:

prompt_json  -> {"messages": [{"role": "user", "content": {"$r": [{"$": "f1"}, "\n\nSummarize the note."]}}]}
response_json -> {"choices": [{"message": {"content": {"$r": ["Echoed: ", {"$": "f1"}]}}}]}

uncondense_json (the inverse in the condense-json library) isn't imported anywhere, so consumers of the --json output now get {"$": ...} / {"$r": [...]} marker objects where they used to get the literal content. I saw the prototype in #760 already showed markers, so this may well be intended (the readable text still lives in the prompt / response columns and the fragments table). Wanted to flag it as a possible compatibility consideration for anyone parsing prompt_json / response_json programmatically, and check whether reads are meant to uncondense_json back to full JSON.

Happy to send a PR for #1 (and #2) if the direction looks right — didn't want to cold-PR a change to the on-disk format.


Reproduced against main (llm 0.32a3 / release 0.31.1), Python 3.14.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions