Skip to content

Python: [Bug]: Checkpoint encoding silently loses values when dictionary keys collide after string conversion #8256

Closed

Description

@fzfzzfzzzfzzzz

Description

Checkpoint encoding converts every dictionary key with str(k) before writing JSON. Distinct Python keys can therefore collapse to the same JSON key and silently overwrite checkpoint state. For example, 1 and "1" are distinct dictionary keys, but both encode as "1".

The existing test documents conversion of ordinary non-string keys to strings, so this report is specifically about collision and silent data loss rather than requesting an unconditional behavior change for all non-string keys.

Expected behavior: checkpoint encoding should either preserve a dictionary whose keys collide after string conversion (for example, by using the existing pickle envelope for that dictionary) or reject it explicitly. It should not silently discard a value.

I would like maintainer guidance on the compatibility approach before submitting a PR. A narrow option is to pickle only dictionaries for which len({str(k) for k in value}) != len(value), preserving the existing behavior for non-colliding keys.

Code Sample

import asyncio
import tempfile

from agent_framework import FileCheckpointStorage, WorkflowCheckpoint


async def main() -> None:
    with tempfile.TemporaryDirectory() as directory:
        storage = FileCheckpointStorage(directory)
        checkpoint = WorkflowCheckpoint(
            workflow_name="wf",
            graph_signature_hash="hash",
            state={"mapping": {1: "integer-key", "1": "string-key"}},
        )

        await storage.save(checkpoint)
        restored = await storage.load(checkpoint.checkpoint_id)

        print(checkpoint.state["mapping"])
        print(restored.state["mapping"])


asyncio.run(main())

Output:

{1: 'integer-key', '1': 'string-key'}
{'1': 'string-key'}

Error Messages / Stack Traces

No exception or warning is emitted; one value is silently lost.

Package Versions

agent-framework-core: source checkout of main at d7823b2

Python Version

Python 3.11.9

Additional Context

The module describes the hybrid format as providing full Python object fidelity for data values, and it already pickles dictionaries containing reserved marker keys. Reusing the pickle envelope only for string-key collisions would avoid introducing a new serialization format.

AI assistance was used to audit the encoding path and prepare the reproduction; the round-trip result was verified against the referenced source checkout.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 10, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Sep 10, 2026
  3. github-actions commented on Sep 10, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    The bug occurs in python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py::_encode at line 318 when distinct dictionary keys produce the same string. The dictionary comprehension silently retains only the later value. Minimal repro: round-trip {1: "integer-key", "1": "string-key"} through encode_checkpoint_value and decode_checkpoint_value and assert equality.

    • Failing test: python/packages/core/tests/workflow/test_checkpoint_encode.py
    • Files examined: python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py, python/packages/core/agent_framework/_workflows/_checkpoint.py, python/packages/core/tests/workflow/test_checkpoint_encode.py, python/packages/core/pyproject.toml
    • Tests run: test_encode_dict_with_non_string_keys, test_encode_round_trips_dict_with_colliding_stringified_keys, test_checkpoint_encode.py
    • Reported version: main@d7823b2
    • Current version: 1.18.0
  4. added
    workflowsUsage: [Issues, PRs], Target: Workflows
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 10, 2026
  5. he-yufeng commented on Sep 14, 2026

    @he-yufeng
    Contributor

    Verified on current main (0ff9e40): _encode in python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py still does encoded_dict = {str(k): _encode(v) for k, v in typed_dict.items()}, so 1 and "1" collide into one JSON key and the earlier value is silently overwritten.

    On the compatibility question you asked about: the narrow shape you sketched is the right one, and it has an in-tree precedent. The encoder already pickles a dict wholesale when any key stringifies into _RESERVED_DICT_KEYS (__pickled__ / __type__). Extending that trigger to len({str(k) for k in value}) != len(value) keeps every existing checkpoint byte-identical (non-colliding dicts take the plain path exactly as today) and only collisions take the envelope. On decode, the envelope preserves the original key types, while checkpoints written before the fix decode as they always did — the already-collapsed value stays collapsed, which is the honest outcome for data written with the loss.

    One nuance worth pinning in the PR's tests: the whole dict rides in one envelope, so a custom object nested under a colliding dict is unpickled under the restricted loader's allowed types just as it would be standalone — same security posture, but worth a test with allowed_types so nobody "fixes" that later by widening it.

    Happy to leave this to you since you said you wanted the PR. If you'd rather not carry it, say the word and I'll pick it up.

  6. ktz03 commented on Sep 15, 2026

    @ktz03
    Contributor

    Fixed by #8272 (merged). The merged PR body mistakenly said Fixes #8200; this issue (#8256) was the intended target.

  7. he-yufeng commented on Sep 15, 2026

    @he-yufeng
    Contributor

    Confirming from the merged diff: #8272 resolves this exactly along the lines analyzed above. Stringified-key collisions now fall back to pickle encoding (so {1: "a", "1": "b"} survives the round trip), and each key is stringified once and reused across the reserved-key check, the collision check, and the encode pass, which removes the stateful-str disagreement between the three reads. The new tests in test_checkpoint_encode.py pin both the int/str collision and the pickle fallback. Nothing left on this one from my side; thanks for the quick pickup, and noting the body reference should read #8256 rather than #8200 for anyone tracing it later.

  8. eavanvalkenburg commented on Sep 16, 2026

    @eavanvalkenburg
    Member

    Resolved by #8272, which detects stringified dictionary-key collisions and preserves those dictionaries through the checkpoint pickle envelope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

pythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowworkflowsUsage: [Issues, PRs], Target: Workflows

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions