fix(agent): persist messages by intrinsic marker to stop id() reuse data loss - #50372
Closed
rrevenanttt wants to merge 1 commit into
Closed
fix(agent): persist messages by intrinsic marker to stop id() reuse data loss#50372rrevenanttt wants to merge 1 commit into
rrevenanttt wants to merge 1 commit into
Conversation
…ata loss ## What does this PR do? `_flush_messages_to_session_db` decided which messages were already durable purely by object identity: `if id(msg) in flushed_ids: continue`. On a long-lived `AIAgent` (CLI/TUI/gateway) that dedup set survives across turns, and that is dangerous. Once a previously-flushed message dict is dropped from the live list — which happens routinely via `_drop_trailing_empty_response_scaffolding` popping orphaned tool/assistant pairs, in-place compaction replacing tool outputs, and `repair_message_sequence` compaction — the dict is garbage-collected and CPython is free to hand its address to a brand-new message. The new (real) assistant or tool message then has an `id()` that already lives in `flushed_ids`, so it is treated as already-written and is silently never persisted to `state.db`. The impact is severe and hard to detect: non-deterministic, silent loss of real conversation turns from the durable transcript. The user sees a normal session, but on resume the turn is simply gone — no error, no warning. Because it depends on allocator address reuse it surfaces intermittently, exactly the kind of corruption that erodes trust in session persistence. The fix removes the reusable address from the trust path entirely. Persistence is now tracked with an intrinsic per-message marker (`_db_persisted`) stamped on the dict itself the moment it is written. A marker bound to the object cannot be aliased onto a recycled address, so a real turn can never be skipped. This is safe by construction: the marker uses the mandatory `_` prefix, and the wire sanitizers already strip every top-level `_`-prefixed key before a request leaves the process, so it never reaches a strict OpenAI-compatible gateway. The existing `_flushed_db_message_ids` seed contract (gateway shutdown, tests populate it with live-object ids right before the flush) is preserved: those ids are valid at seed time, so we translate them into durable markers once and then clear the set, guaranteeing no stale id can accumulate across turns and alias a future message. ## Related Issue N/A ## Type of Change - [x] 🐛 Bug fix (non-breaking change that fixes an issue) - [ ] ✨ New feature (non-breaking change that adds functionality) - [ ] 🔒 Security fix - [ ] 📝 Documentation update - [ ] ✅ Tests (adding or improving test coverage) - [ ] ♻️ Refactor (no behavior change) - [ ] 🎯 New skill (bundled or hub) ## Changes Made - `run_agent.py`: added the `_DB_PERSISTED_MARKER` constant documenting the id()-reuse hazard and the wire-sanitizer guarantee. - `run_agent.py`: rewrote the dedup loop in `_flush_messages_to_session_db` to skip on the intrinsic `_db_persisted` marker instead of `id(msg)`, treat `_flushed_db_message_ids` as a one-shot seed (translated to markers), and clear that set after every flush so no recyclable id outlives its turn. - `tests/run_agent/test_identity_flush.py`: added `test_flush_does_not_retain_object_ids_across_turns` and `test_recycled_id_in_dedup_set_still_persists_new_message` covering the data loss and the marker-based dedup contract. ## How to Test 1. Run the focused suite: `scripts/run_tests.sh tests/run_agent/test_identity_flush.py`. 2. The two new tests fail against the old `id()`-keyed logic (a stale id aliases a fresh message and it is dropped) and pass with the marker-based dedup. 3. Regression sweep — persistence, dedup and compaction paths stay green: `scripts/run_tests.sh tests/run_agent/ tests/gateway/test_13121_shutdown_inflight_transcript_flush.py tests/gateway/test_agent_cache.py`. ## Checklist ### Code - [x] I've read the [Contributing Guide](https://github.com/NousResearch/hermes-agent/blob/main/CONTRIBUTING.md) - [x] My commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) (`fix(scope):`, `feat(scope):`, etc.) - [x] I searched for [existing PRs](https://github.com/NousResearch/hermes-agent/pulls) to make sure this isn't a duplicate - [x] My PR contains **only** changes related to this fix/feature (no unrelated commits) - [x] I've run `pytest tests/ -q` and all tests pass - [x] I've added tests for my changes (required for bug fixes, strongly encouraged for features) - [x] I've tested on my platform: macOS 15 (Darwin 25.5), Python 3.13 ### Documentation & Housekeeping - [x] I've updated relevant documentation (README, `docs/`, docstrings) — or N/A - [x] I've updated `cli-config.yaml.example` if I added/changed config keys — or N/A - [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — or N/A - [x] I've considered cross-platform impact (Windows, macOS) per the [compatibility guide](https://github.com/NousResearch/hermes-agent/blob/main/CONTRIBUTING.md#cross-platform-compatibility) — or N/A - [x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
kshitijk4poor
pushed a commit
that referenced
this pull request
Jul 1, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from #50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
kshitijk4poor
pushed a commit
to kshitijk4poor/hermes-agent
that referenced
this pull request
Jul 1, 2026
… list (NousResearch#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in NousResearch#56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from NousResearch#48817; chokepoint reworked to coexist with the marker-based dedup (NousResearch#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
kshitijk4poor
pushed a commit
that referenced
this pull request
Jul 1, 2026
… list (#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in #56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from #48817; chokepoint reworked to coexist with the marker-based dedup (#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from NousResearch#50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
… list (NousResearch#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in NousResearch#56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from NousResearch#48817; chokepoint reworked to coexist with the marker-based dedup (NousResearch#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
caozuohua
pushed a commit
to caozuohua/hermes-agent
that referenced
this pull request
Jul 2, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from NousResearch#50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
(cherry picked from commit e4c6d1b)
2 tasks
Jasper6439
pushed a commit
to Jasper6439/hermes-agent
that referenced
this pull request
Jul 5, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from NousResearch#50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
Jasper6439
pushed a commit
to Jasper6439/hermes-agent
that referenced
this pull request
Jul 5, 2026
… list (NousResearch#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in NousResearch#56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from NousResearch#48817; chokepoint reworked to coexist with the marker-based dedup (NousResearch#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from NousResearch#50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
… list (NousResearch#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in NousResearch#56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from NousResearch#48817; chokepoint reworked to coexist with the marker-based dedup (NousResearch#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from NousResearch#50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
… list (NousResearch#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in NousResearch#56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from NousResearch#48817; chokepoint reworked to coexist with the marker-based dedup (NousResearch#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from NousResearch#50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
… list (NousResearch#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in NousResearch#56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from NousResearch#48817; chokepoint reworked to coexist with the marker-based dedup (NousResearch#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…ata loss
_flush_messages_to_session_db deduped persisted messages with a retained
{id(msg)} set (_flushed_db_message_ids) kept across turns. Once a flushed dict
is dropped from the live list (scaffolding rewind / in-place compaction) and
GC'd, CPython recycles its address onto a new assistant/tool dict whose id()
collides with the stale entry — so the real turn is silently never written to
state.db.
Replace the retained id-set with an intrinsic _DB_PERSISTED_MARKER stamped on
each dict. The id-set is demoted to a one-shot seed (valid only while the
caller's objects are alive) that is translated to markers and cleared after
every flush, so no id() outlives a flush to alias a future message. The marker
is _-prefixed so the wire sanitizers strip it before any request leaves.
Preserves the existing _is_ephemeral_scaffolding skip. Salvaged from NousResearch#50372.
Co-authored-by: rrevenanttt <290873280+rrevenanttt@users.noreply.github.com>
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
… list (NousResearch#48677) The persist user-message override was applied in place to the live messages list. On the early crash-resilience persist (which runs BEFORE api_messages is built), that stripped observed group-chat context off the live user message and silently dropped it when observe_unmentioned_group_messages was enabled. Fix at the single chokepoint: _flush_messages_to_session_db resolves the override (idx/content/timestamp) locally and applies it ONLY to the row written to the DB — the live dict is never mutated, so EVERY persist caller (early persist, mid tool-loop flush, /resume, /branch) is protected uniformly. This supersedes the earlier shallow-copy approach, which broke the intrinsic _DB_PERSISTED_MARKER idempotency (copies never propagated the marker back to the live dicts → duplicate rows) and closes the sibling class tracked in NousResearch#56303. Trailing empty-response scaffolding is still dropped from the live list in _persist_session (unchanged behavior). Salvaged from NousResearch#48817; chokepoint reworked to coexist with the marker-based dedup (NousResearch#50372). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
_flush_messages_to_session_dbdecided which messages were already durablepurely by object identity:
if id(msg) in flushed_ids: continue. On along-lived
AIAgent(CLI/TUI/gateway) that dedup set survives across turns,and that is dangerous. Once a previously-flushed message dict is dropped from
the live list — which happens routinely via
_drop_trailing_empty_response_scaffoldingpopping orphaned tool/assistantpairs, in-place compaction replacing tool outputs, and
repair_message_sequencecompaction — the dict is garbage-collected and CPython is free to hand its
address to a brand-new message. The new (real) assistant or tool message then
has an
id()that already lives influshed_ids, so it is treated asalready-written and is silently never persisted to
state.db.The impact is severe and hard to detect: non-deterministic, silent loss of
real conversation turns from the durable transcript. The user sees a normal
session, but on resume the turn is simply gone — no error, no warning. Because
it depends on allocator address reuse it surfaces intermittently, exactly the
kind of corruption that erodes trust in session persistence.
The fix removes the reusable address from the trust path entirely. Persistence
is now tracked with an intrinsic per-message marker (
_db_persisted) stampedon the dict itself the moment it is written. A marker bound to the object
cannot be aliased onto a recycled address, so a real turn can never be skipped.
This is safe by construction: the marker uses the mandatory
_prefix, and thewire sanitizers already strip every top-level
_-prefixed key before a requestleaves the process, so it never reaches a strict OpenAI-compatible gateway.
The existing
_flushed_db_message_idsseed contract (gateway shutdown, testspopulate it with live-object ids right before the flush) is preserved: those
ids are valid at seed time, so we translate them into durable markers once and
then clear the set, guaranteeing no stale id can accumulate across turns and
alias a future message.
Related Issue
N/A
Type of Change
Changes Made
run_agent.py: added the_DB_PERSISTED_MARKERconstant documenting theid()-reuse hazard and the wire-sanitizer guarantee.
run_agent.py: rewrote the dedup loop in_flush_messages_to_session_dbtoskip on the intrinsic
_db_persistedmarker instead ofid(msg), treat_flushed_db_message_idsas a one-shot seed (translated to markers), andclear that set after every flush so no recyclable id outlives its turn.
tests/run_agent/test_identity_flush.py: addedtest_flush_does_not_retain_object_ids_across_turnsandtest_recycled_id_in_dedup_set_still_persists_new_messagecovering the dataloss and the marker-based dedup contract.
How to Test
scripts/run_tests.sh tests/run_agent/test_identity_flush.py.id()-keyed logic (a stale id aliasesa fresh message and it is dropped) and pass with the marker-based dedup.
scripts/run_tests.sh tests/run_agent/ tests/gateway/test_13121_shutdown_inflight_transcript_flush.py tests/gateway/test_agent_cache.py.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A