Skip to content

fix(context-manager): never stash an offload preview under its original's key - #5623

Closed
strandly-the-agent wants to merge 2 commits into
strands-agents:mainfrom
strandly-the-agent:agent-tasks/5620
Closed

strandly-the-agent wants to merge 2 commits into
strands-agents:mainfrom
strandly-the-agent:agent-tasks/5620

Conversation

@strandly-the-agent

Copy link
Copy Markdown
Collaborator

Requested by: @ekumar96 — Slack DM 2026-10-28: after a session restore onto a host whose stash no longer holds the originals, retrieve_context returns the truncated preview as if it were the original.

Human Overview

Description

After a session restore, ContextManager._backfill_stash runs store_message(..., keep_existing=True) over every restored message. keep_existing only protects keys the stash already holds, so when the stash on the new host is empty (new host, expired store, failed write) an already-truncated tool-result preview is written under the original's key <toolUseId>_0. From then on retrieve_context serves the 9-line preview as the "original": a pattern search reports no matches and a full retrieval returns [Truncated: …], with nothing telling the model the content is gone. #4699 fixed the sibling case where the stash still holds the original; this is the complement.

Two changes. On the backfill path, Stash.store_message now skips a block that is itself the offload marker for the key it would be stored under: every strategy's replacement ends in the [ref: …] / [refs: …] token that only _format_stash_refs produces, so a block whose trailing token names its own key stands in for an original and must not be stored in its place. The live MessageAddedEvent path is unchanged, and an original that merely cites another entry is still stored. Separately, retrieve_context refuses to serve a stashed entry that is the marker for the requested reference ("original content … is no longer available; only its offloaded preview remains"), which also covers durable stores already polluted by 1.62.0. TypeScript has no backfill step and is unaffected.

Related Issues

Fixes #5620

Documentation PR

N/A

Type of Change

Bug fix

Testing

SDK-only repro (two ContextManagers, the second seeded with the first one's messages and an empty stash) returns the preview on main and Error: reference not found with this change — script and both outputs are attached to the issue card. New unit tests: backfill onto an empty stash leaves the original's key empty while still stashing plain blocks; _is_offload_marker against every strategy's output and against originals that cite another ref; retrieval guard rejects a stashed preview but serves an original that cites another ref; bracket-heavy 200 KB input stays bounded. Both behaviour tests fail without the fix.

  • I ran hatch run prepare (equivalent, see details)

Additional Details

Gate output and review ledger
ruff format src tests   -> 674 files left unchanged
ruff check src tests    -> All checks passed!
mypy src/strands/_context_manager -> Success: no issues found in 18 source files
pytest tests/strands/_context_manager -> 315 passed
pytest tests (full)     -> 10 failed, 6183 passed, 38 skipped, 23 errors

The 10 failures (bidi/test_deprecated_alias.py, bidi/agent/test_agent.py, test_agent.py::test_baggage_attributes_stamped_on_agent_span) and 23 collection errors are identical on main in the same venv — missing optional deps (a2a, fastapi, …), unrelated to this change.

Independent review (fresh-context reviewer) raised two 🟡 findings, both fixed in the second commit:

  • Regex \[refs?: [^\]]*_\d+\]$ backtracked quadratically on bracket-heavy text (600 KB → 60 s). Now [^\[\]]*, bounded; test added.
  • A genuine original that happened to end in a [ref: …] token would have been skipped on backfill / rejected on retrieval. The check now requires the token to name the block's own key (refs_for) or the requested reference, so only true self-referencing markers are affected; tests added for both paths.

Known limit: a stash already polluted by 1.62.0 keeps the preview under the original's key (backfill never overwrites); the retrieval guard makes that honest rather than recovering the original, which is gone.

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

…al's key

After a session restore onto a host whose stash no longer holds the originals,
the one-time backfill stored every restored block, including already-truncated
tool-result previews, under the original's reference. retrieve_context then
served the preview as the original. Backfill now skips blocks that are offload
markers, and retrieve_context refuses to serve a stashed entry that is itself a
marker, which also covers stores already polluted by earlier releases.

Fixes strands-agents#5620
…its own key

Review follow-up: bound the ref-token regex (no backtracking across '[') and
require the marker to reference the key it would be stored under, so an
original that merely cites another stash entry is still stored and served.
@strandly-the-agent
strandly-the-agent requested a review from a team as a code owner October 28, 2026 16:24
@github-actions github-actions Bot added bug Something isn't working python Pull requests that update python code area-context Session or context related area-persistence Session management or checkpointing labels Oct 28, 2026
@lizradway

Copy link
Copy Markdown
Member

closing out in favor of #5626 which resolves in py and ts

@lizradway lizradway closed this Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-context Session or context related area-persistence Session management or checkpointing bug Something isn't working python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] ContextManager: after a session restore onto an empty stash, retrieve_context serves the truncated preview as the original

2 participants