Skip to content

fix(context): never stash or serve offload previews as originals (py + ts) - #5626

Merged
lizradway merged 2 commits into
strands-agents:mainfrom
lizradway:lizradway/explain-issue-5620
Oct 28, 2026
Merged

lizradway merged 2 commits into
strands-agents:mainfrom
lizradway:lizradway/explain-issue-5620

Conversation

@lizradway

Copy link
Copy Markdown
Member

Human Overview

Description

When a conversation holding offloaded previews lands on a stash that doesn't have the originals, the stash write path files each preview under its original's key, because a truncated tool result keeps its toolUseId. retrieve_context then serves the preview as the original, and the model reports "No matches found" for content that exists. In Python this happens through backfill after a session restore onto an empty stash (new host, expired store, failed write). In TS it happens when an agent is seeded via new Agent({ messages }) with already-offloaded messages, and since TS store always overwrites, a shared durable stash loses its real originals.

Both SDKs now skip offload placeholders when stashing, recognized by the trailing [ref: …] / [refs: …] token that _format_stash_refs / formatStashRefs adds to every offload marker. When any item of a tool result is a placeholder, the whole result is skipped, because truncation re-indexes the remaining items. retrieve_context also refuses entries that are placeholders, which covers stashes 1.62.0 already polluted. A lost original now surfaces as an error instead of a confident wrong answer.

Related Issues

Fixes #5620

Documentation PR

N/A

Type of Change

Bug fix

Testing

Ran the Python and TS context-manager unit suites. The new tests fail on main and pass with the fix. End to end in Python: host A truncates a 5,000-row tool result, and host B gets A's conversation with an empty stash. retrieve_context(reference="tu-1_0", pattern="row-4321") returns success: No matches found for pattern 'row-4321' on main and error: reference not found: tu-1_0 with this change. TS was exercised through ContextManager.initAgent with seeded messages over a stash that holds the original, not with a live model.

  • I ran hatch run prepare (ran hatch run format, hatch run lint, and the context-manager tests separately; the full prepare was killed locally for memory)

Additional Details

Risks and repro script

False positive: a genuine tool output or text block that ends with the literal " [ref: x]" is no longer eagerly stashed, so if it's offloaded later, retrieval reports "not found". The match is anchored to the end of the string with a leading space, so mid-text mentions are still stored.

Not in scope: skipping backfill after restore was considered and rejected. It couples the session manager to context-manager internals and misses the seeded-messages path.

Repro, run from strands-py/:

import asyncio, copy, sys
sys.path.append("tests/fixtures")
from mocked_model_provider import MockedModelProvider
from strands import Agent
from strands._context_manager.context_manager import ContextManager
from strands._context_manager.retrieval_tool import _create_retrieval_tool
from strands._context_manager.strategies.offload import Offload

ROWS = "\n".join(f"row-{i}" for i in range(1, 5001))
seed = [
    {"role": "user", "content": [{"text": "list the rows"}]},
    {"role": "assistant", "content": [{"toolUse": {"toolUseId": "tu-1", "name": "bash", "input": {}}}]},
    {"role": "user", "content": [{"toolResult": {"toolUseId": "tu-1", "status": "success", "content": [{"text": ROWS}]}}]},
    {"role": "assistant", "content": [{"text": "done"}]},
]
reply = lambda: MockedModelProvider([{"role": "assistant", "content": [{"text": "ok"}]}])

async def main():
    truncate = Offload.truncate("tool_results", {"preview_tokens": 100}).when(threshold=200)
    host_a = Agent(model=reply(), messages=seed, context_manager=ContextManager(strategies=[truncate]), callback_handler=None)
    await host_a.invoke_async("go")
    cm_b = ContextManager(strategies=[])  # same conversation, empty stash
    host_b = Agent(model=reply(), messages=copy.deepcopy(host_a.messages[:4]), context_manager=cm_b, callback_handler=None)
    await host_b.invoke_async("go")
    tool = _create_retrieval_tool(cm_b.stash)
    print(await tool._tool_func({"toolUseId": "r", "input": {"reference": "tu-1_0", "pattern": "row-4321"}}))

asyncio.run(main())

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.

🤖 Generated with Claude Code

Backfill after a session restore onto a stash lacking the originals wrote
the restored previews under the originals' keys, so retrieve_context served
the preview as the original. store_message now skips offload placeholders,
and retrieve_context reports a placeholder entry as no longer available.

Fixes strands-agents#5620
Seeding an agent with already-offloaded messages stashed the previews under
the originals' keys, overwriting the originals in a shared stash. storeMessage
now skips offload placeholders, and retrieve_context reports a placeholder
entry as no longer available.

Refs strands-agents#5620
@github-actions github-actions Bot added area-context Session or context related area-persistence Session management or checkpointing bug Something isn't working labels Oct 28, 2026
@lizradway
lizradway marked this pull request as ready for review October 28, 2026 16:59
@lizradway
lizradway requested a review from a team as a code owner October 28, 2026 16:59
@lizradway
lizradway requested a review from liramon2 October 28, 2026 16:59
@github-actions github-actions Bot added complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting size/s strands-running labels Oct 28, 2026
@codecov

codecov Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve

Focused, well-scoped bug fix for #5620 with parallel Python/TS implementations and the detection logic coupled to the same _format_stash_refs/formatStashRefs the offload strategies use, so recognition can't silently drift from generation. Tests are strong — both SDKs, full-object assertions (take_snapshot() == {…} / toStrictEqual), and good negative/edge cases.

Review notes
  • Correctness: The whole-tool-result skip when any item is a placeholder, and the empty-stash "surfaces as error, not a wrong answer" behavior, are both correct and already explained in the PR description. The empty-refs marker case only arises when the stash is disabled, so it's benign.
  • Design: Only substantive note is the string-suffix detection mechanism — see the inline suggestion; non-blocking given the documented mitigations.
  • Coverage: Codecov reports ~30% patch coverage, but the new error branch and skip branches do appear covered by the added tests. Worth a quick glance to confirm the coverage job is picking up the new tests rather than a real gap.

Clear write-up and repro script made this easy to verify — thanks.

@lizradway
lizradway enabled auto-merge (squash) October 28, 2026 17:05
@lizradway
lizradway merged commit baf15f5 into strands-agents:main Oct 28, 2026
77 of 107 checks passed
@lizradway
lizradway deleted the lizradway/explain-issue-5620 branch October 28, 2026 17:46
lizradway added a commit to lizradway/harness-sdk that referenced this pull request Oct 29, 2026
Resolves the strands-agents#5626 conflicts in favor of the typed offload placeholder check, which replaces the TS ref regex guard.
gautamsirdeshmukh pushed a commit to gautamsirdeshmukh/harness-sdk that referenced this pull request Nov 2, 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 complexity/high A touched function exceeds cognitive complexity 25; may be worth splitting size/s

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