Intermediate assistant messages (with tool_calls) and tool results are never sent to the user but stay in the model's context, causing the model to reference content the user never saw
Adds _hidden_sig field to intermediate messages at creation time in context.py
Applies [HIDDEN:{sig}] prefix at read time in session.get_history() so the model sees which messages were hidden
Storing signature separately from content preserves Anthropic prompt caching — same prefixed string produced every turn
Files changed
nanobot/agent/visibility.py — new compute_signature() function (returns hex only)
nanobot/agent/context.py — add_assistant_message() and add_tool_result() store _hidden_sig
nanobot/session/manager.py — get_history() applies [HIDDEN:sig] prefix at read time
Test plan
All 277 tests pass
Deploy to staging and verify model stops referencing unseen messages
Verify prompt cache hit rate is not degraded
## Summary
- Intermediate assistant messages (with tool_calls) and tool results are never sent to the user but stay in the model's context, causing the model to reference content the user never saw
- Adds `_hidden_sig` field to intermediate messages at creation time in `context.py`
- Applies `[HIDDEN:{sig}]` prefix at read time in `session.get_history()` so the model sees which messages were hidden
- Storing signature separately from content preserves Anthropic prompt caching — same prefixed string produced every turn
## Files changed
- `nanobot/agent/visibility.py` — new `compute_signature()` function (returns hex only)
- `nanobot/agent/context.py` — `add_assistant_message()` and `add_tool_result()` store `_hidden_sig`
- `nanobot/session/manager.py` — `get_history()` applies `[HIDDEN:sig]` prefix at read time
## Test plan
- [x] All 277 tests pass
- [ ] Deploy to staging and verify model stops referencing unseen messages
- [ ] Verify prompt cache hit rate is not degraded
Intermediate assistant messages (with tool_calls) and tool result messages
are never sent to the user but remain in the model's context. This causes
the model to refer to content the user never saw.
Add _hidden_sig field at message creation time (context.py), then apply
[HIDDEN:sig] prefix at read time (session get_history) so the model sees
which messages were hidden. Storing the signature separately from content
preserves Anthropic prompt caching — the same prefixed string is produced
every turn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The split between write-time signature storage (_hidden_sig) and read-time prefix application (get_history()) is a good call for prompt caching — content bytes in the session file stay stable, and Anthropic's cache sees identical prefixes across turns.
No double-signing risk: suppress_output messages go through sign_content (no tool_calls → no _hidden_sig), while intermediate messages go through _hidden_sig (never through sign_content). The two paths don't overlap.
sign_content() (visibility.py:31-35) has its own inline HMAC computation identical to the new compute_signature(). Two copies of the same HMAC logic will drift. Refactor:
All 15 existing tests cover the pre-existing sign_content/suppress_output system. None test what this PR changes:
compute_signature() directly
_hidden_sig being added by add_tool_result() and add_assistant_message(tool_calls=...)
get_history() applying [HIDDEN:{sig}] prefix when _hidden_sig is present
Messages WITHOUT _hidden_sig NOT getting prefixed
Round-trip: write to session JSONL → reload → get_history() still produces correct prefix
Optional improvements
3. _hidden_sig not verified at read time
In get_history() (manager.py:73-75), sig from _hidden_sig is interpolated directly into the prefix without checking it matches the content. In contrast, the suppress_output path verifies via has_forged_marker(). Low priority since session files are local, but worth noting the asymmetry.
4. All tool results get _hidden_sig unconditionally
add_tool_result() always adds _hidden_sig. When a user says "read file X", the tool result is marked [HIDDEN] even though the user explicitly requested that action. If intentional, the system prompt visibility section should clarify: "[HIDDEN] means the raw result wasn't sent verbatim, not that the user is unaware of the action."
5. Minor: Tuple import (pre-existing)
visibility.py:7 uses from typing import Tuple — project convention is tuple lowercase (Python 3.11+). Not introduced by this PR but worth cleaning up while touching the file.
## Code Review
### Design
The split between write-time signature storage (`_hidden_sig`) and read-time prefix application (`get_history()`) is a good call for prompt caching — content bytes in the session file stay stable, and Anthropic's cache sees identical prefixes across turns.
No double-signing risk: suppress_output messages go through `sign_content` (no tool_calls → no `_hidden_sig`), while intermediate messages go through `_hidden_sig` (never through `sign_content`). The two paths don't overlap.
### Issues to fix before merge
**1. `compute_signature` duplicates `sign_content` internals**
`sign_content()` (visibility.py:31-35) has its own inline HMAC computation identical to the new `compute_signature()`. Two copies of the same HMAC logic will drift. Refactor:
```python
def sign_content(content: str) -> str:
sig = compute_signature(content)
return f"[HIDDEN:{sig}] {content}"
```
**2. No tests for the actual changes in this PR**
All 15 existing tests cover the pre-existing `sign_content`/suppress_output system. None test what this PR changes:
- `compute_signature()` directly
- `_hidden_sig` being added by `add_tool_result()` and `add_assistant_message(tool_calls=...)`
- `get_history()` applying `[HIDDEN:{sig}]` prefix when `_hidden_sig` is present
- Messages WITHOUT `_hidden_sig` NOT getting prefixed
- Round-trip: write to session JSONL → reload → `get_history()` still produces correct prefix
### Optional improvements
**3. `_hidden_sig` not verified at read time**
In `get_history()` (manager.py:73-75), `sig` from `_hidden_sig` is interpolated directly into the prefix without checking it matches the content. In contrast, the suppress_output path verifies via `has_forged_marker()`. Low priority since session files are local, but worth noting the asymmetry.
**4. All tool results get `_hidden_sig` unconditionally**
`add_tool_result()` always adds `_hidden_sig`. When a user says "read file X", the tool result is marked `[HIDDEN]` even though the user explicitly requested that action. If intentional, the system prompt visibility section should clarify: "[HIDDEN] means the raw result wasn't sent verbatim, not that the user is unaware of the action."
**5. Minor: `Tuple` import (pre-existing)**
`visibility.py:7` uses `from typing import Tuple` — project convention is `tuple` lowercase (Python 3.11+). Not introduced by this PR but worth cleaning up while touching the file.
Intermediate assistant messages (with tool_calls) and tool result messages
are never sent to the user but remain in the model's context. This causes
the model to refer to content the user never saw.
Add _hidden_sig field at message creation time (context.py), then apply
[HIDDEN:sig] prefix at read time (session get_history) so the model sees
which messages were hidden. Storing the signature separately from content
preserves Anthropic prompt caching — the same prefixed string is produced
every turn.
Changes:
- visibility.py: add compute_signature(), refactor sign_content/verify to
use it, fix Tuple -> tuple (PEP 585)
- context.py: add_assistant_message() and add_tool_result() store _hidden_sig
- session/manager.py: get_history() applies [HIDDEN:sig] prefix at read time
- tests/test_message_visibility.py: 14 tests covering compute_signature,
_hidden_sig creation, get_history prefix, JSONL round-trip, idempotency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
_hidden_sigfield to intermediate messages at creation time incontext.py[HIDDEN:{sig}]prefix at read time insession.get_history()so the model sees which messages were hiddenFiles changed
nanobot/agent/visibility.py— newcompute_signature()function (returns hex only)nanobot/agent/context.py—add_assistant_message()andadd_tool_result()store_hidden_signanobot/session/manager.py—get_history()applies[HIDDEN:sig]prefix at read timeTest plan
32faed5c1ctod90c3b4a24Code Review
Design
The split between write-time signature storage (
_hidden_sig) and read-time prefix application (get_history()) is a good call for prompt caching — content bytes in the session file stay stable, and Anthropic's cache sees identical prefixes across turns.No double-signing risk: suppress_output messages go through
sign_content(no tool_calls → no_hidden_sig), while intermediate messages go through_hidden_sig(never throughsign_content). The two paths don't overlap.Issues to fix before merge
1.
compute_signatureduplicatessign_contentinternalssign_content()(visibility.py:31-35) has its own inline HMAC computation identical to the newcompute_signature(). Two copies of the same HMAC logic will drift. Refactor:2. No tests for the actual changes in this PR
All 15 existing tests cover the pre-existing
sign_content/suppress_output system. None test what this PR changes:compute_signature()directly_hidden_sigbeing added byadd_tool_result()andadd_assistant_message(tool_calls=...)get_history()applying[HIDDEN:{sig}]prefix when_hidden_sigis present_hidden_sigNOT getting prefixedget_history()still produces correct prefixOptional improvements
3.
_hidden_signot verified at read timeIn
get_history()(manager.py:73-75),sigfrom_hidden_sigis interpolated directly into the prefix without checking it matches the content. In contrast, the suppress_output path verifies viahas_forged_marker(). Low priority since session files are local, but worth noting the asymmetry.4. All tool results get
_hidden_sigunconditionallyadd_tool_result()always adds_hidden_sig. When a user says "read file X", the tool result is marked[HIDDEN]even though the user explicitly requested that action. If intentional, the system prompt visibility section should clarify: "[HIDDEN] means the raw result wasn't sent verbatim, not that the user is unaware of the action."5. Minor:
Tupleimport (pre-existing)visibility.py:7usesfrom typing import Tuple— project convention istuplelowercase (Python 3.11+). Not introduced by this PR but worth cleaning up while touching the file.