HERMES-GIELIE/tests/test_issue3831_watermark_clear.py

244 lines
10 KiB
Python
Raw Normal View History

fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
"""Regression tests for #3831 — stale truncation_watermark data loss.
Root cause: retry_last / undo_last / the Edit-truncate handler set
``truncation_watermark`` to suppress the *replaced* tail from the append-only
state.db merge. ``Session.save()`` deliberately does not auto-clear it (#2914),
but nothing cleared it when the user then sent a genuinely new turn either — so
it froze at the old edit boundary. A frozen watermark then dropped post-watermark
state.db rows whenever the sidecar was later reconstructed empty
(recovery/reconcile), permanently losing the turns sent after the edit.
Fix: advance a *positive* watermark to the newest user message timestamp once
the new user turn is COMMITTED to ``session.messages`` (the success-merge,
eager-checkpoint, and recovery/cold-load commit points) — NOT at chat-start.
Clearing at chat-start was unsafe: in deferred mode (the default) the new user
row isn't in messages yet, so a merge in that window would resurrect the replaced
tail (the max-sidecar guard hasn't risen past the old boundary). Once the row is
committed, ``max_sidecar_timestamp`` rises past the replaced tail and the merge
suppresses it without the watermark, so advancing it is both safe and necessary.
Advancing to the new message timestamp (instead of clearing to None) keeps the
merge's watermark filter active for replaced pre-edit rows from state.db whose
timestamps fall below the boundary, while the sidecar_advanced_past_watermark
guard allows newer state.db rows to merge in.
``0.0`` is the truncate-to-empty sentinel (#2914) that must keep blocking all
state replay, so the advance is falsy-gated and never touches ``0.0``.
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
"""
import api.models as models
import api.streaming as streaming
def _rows(*specs):
return [
{"role": role, "content": content, "timestamp": ts}
for (role, content, ts) in specs
]
class _FakeSession:
def __init__(self, watermark):
self.truncation_watermark = watermark
self.messages = []
self.context_messages = []
self.pending_user_message = None
self.pending_attachments = None
self.pending_started_at = None
# --- The fix: a committed new turn advances a stale positive watermark --------
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
def test_advance_helper_advances_positive_watermark():
"""After a new user turn is committed, the watermark advances to the
newest user message timestamp — NOT cleared to None."""
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
s = _FakeSession(100.0) # stale, from a prior retry/undo/edit
s.messages = _rows(("user", "new turn", 200))
streaming._advance_truncation_watermark_after_commit(s)
assert s.truncation_watermark == 200.0
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
def test_advance_helper_leaves_zero_watermark_untouched():
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
"""0.0 is the truncate-to-empty sentinel (#2914), not a stale boundary — the
falsy guard must leave it alone so #2914 replay-blocking is preserved."""
s = _FakeSession(0.0)
s.messages = _rows(("user", "new turn", 200))
streaming._advance_truncation_watermark_after_commit(s)
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
assert s.truncation_watermark == 0.0
def test_advance_helper_noop_when_unset():
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
s = _FakeSession(None)
streaming._advance_truncation_watermark_after_commit(s)
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
assert s.truncation_watermark is None
def test_advance_helper_uses_current_time_when_no_timestamp():
"""When the newest user message has no timestamp, the helper falls back to
time.time()."""
s = _FakeSession(100.0)
s.messages = _rows(("user", "new turn", None))
streaming._advance_truncation_watermark_after_commit(s)
assert s.truncation_watermark is not None
assert s.truncation_watermark > 100.0 # advanced past the old watermark
def test_advance_helper_picks_newest_user_timestamp():
"""When there are multiple user messages, the helper picks the newest one."""
s = _FakeSession(50.0)
s.messages = _rows(
("user", "first", 100),
("assistant", "reply", 101),
("user", "second", 200),
("assistant", "reply2", 201),
)
streaming._advance_truncation_watermark_after_commit(s)
assert s.truncation_watermark == 200.0
def test_recovery_commit_advances_watermark():
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
"""The cold-load / recovery commit path (_append_recovered_pending_turn)
advances a stale positive watermark when it materializes the pending turn."""
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
s = _FakeSession(100.0)
s.pending_user_message = "recovered new turn"
s.pending_attachments = None
models._append_recovered_pending_turn(s, timestamp=200)
assert s.truncation_watermark == 200
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
# The pending turn was committed to messages.
assert any(m.get("content") == "recovered new turn" for m in s.messages)
# --- The effect: advanced watermark still merges post-edit turns --------------
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
def test_advanced_watermark_keeps_post_edit_state_rows():
"""With a sidecar whose max timestamp exceeds the watermark (the real
post-edit state after the assistant reply was appended), post-edit state.db
rows are merged in because sidecar_advanced_past_watermark is True."""
sidecar = _rows(
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
("user", "q1", 50),
("assistant", "a1", 100), # old edit boundary
("user", "q2-new", 200), # new message after edit (watermark=200)
("assistant", "a2-new", 250), # assistant reply — pushes sidecar past watermark
)
state = _rows(
("user", "q1", 50),
("assistant", "a1", 100),
("user", "q2-new", 200),
("assistant", "a2-new", 250),
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
)
merged = models.merge_session_messages_append_only(
sidecar, state, truncation_watermark=200.0
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
)
# max_sidecar_timestamp=250 > watermark=200 → sidecar_advanced_past_watermark=True
# → watermark filter does not block a2-new
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
assert [m["content"] for m in merged] == ["q1", "a1", "q2-new", "a2-new"]
# --- The guardrails: the fix must NOT regress #2914/#3102 ---------------------
def test_active_watermark_still_filters_replaced_tail_empty_sidecar():
2026-06-24 07:13:35 +00:00
"""A positive watermark (advanced to the new user-turn timestamp) with an
empty sidecar must suppress the replaced pre-edit tail while keeping
post-edit rows (#4767 / CORE finding #2).
A genuinely-advanced session persists the original cutoff as
``truncation_boundary`` (@100, the last kept reply before the replaced tail)
alongside the advanced watermark (@200, the new committed turn). The
reconstruction keeps ts <= boundary plus ts >= watermark and drops the
replaced (boundary, watermark) suffix."""
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
state = _rows(
("user", "q1", 50),
("assistant", "a1", 100),
2026-06-24 07:13:35 +00:00
("user", "replaced-q2", 150), # replaced by the edit -> filtered
("assistant", "replaced-a2", 160), # replaced by the edit -> filtered
("user", "edited-q2", 200), # new user turn (watermark advanced here)
("assistant", "a2-new", 300), # post-edit reply -> kept
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
)
merged = models.merge_session_messages_append_only(
[], state, truncation_watermark=200.0, truncation_boundary=100.0
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
)
2026-06-24 07:13:35 +00:00
assert [m["content"] for m in merged] == ["q1", "a1", "edited-q2", "a2-new"]
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
def test_zero_watermark_still_blocks_all_replay_empty_sidecar():
"""A 0.0 watermark on a truncate-to-empty session must STILL block all state
replay (#2914) — unchanged by this fix."""
state = _rows(
("user", "only prompt", 1.0),
("assistant", "only reply", 2.0),
)
merged = models.merge_session_messages_append_only(
[], state, truncation_watermark=0.0
)
assert merged == []
# --- Inline commit-path coverage: the two sites that inline the advance --------
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
def test_error_path_materialize_advances_positive_watermark():
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
"""The error/cancel materialization path (_materialize_pending_user_turn_before_error)
inlines the watermark advance. A pending user turn committed on the error
path advances a stale positive watermark to the recovered timestamp."""
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
s = _FakeSession(100.0) # stale, from a prior retry/undo/edit
s.pending_user_message = "new turn after edit"
appended = streaming._materialize_pending_user_turn_before_error(s)
assert appended is True
assert s.truncation_watermark is not None
assert s.truncation_watermark >= 100.0 # advanced past old watermark
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
assert any(m.get("content") == "new turn after edit" for m in s.messages)
def test_error_path_materialize_preserves_zero_sentinel():
"""The error-path inline must use the same falsy guard as the helper: the 0.0
truncate-to-empty sentinel (#2914) is preserved, not cleared."""
s = _FakeSession(0.0)
s.pending_user_message = "new turn"
streaming._materialize_pending_user_turn_before_error(s)
assert s.truncation_watermark == 0.0
def test_eager_checkpoint_advances_positive_watermark():
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
"""The eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save
in routes.py) inlines the same advance. A committed user turn advances a
stale positive watermark to the new message timestamp; the 0.0 sentinel is
preserved."""
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
import api.routes as routes
s = _FakeSession(100.0)
routes._checkpoint_user_message_for_eager_session_save(
s, "eager new turn", None, started_at=200.0
)
assert s.truncation_watermark == 200.0
fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890) * fix(session): retire stale truncation watermark on new committed turn (#3831) retry_last / undo_last / the Edit-truncate handler set truncation_watermark to suppress the *replaced* tail from the append-only state.db merge. Session.save() deliberately never auto-clears it (#2914), but nothing retired it when the user then sent a genuinely NEW turn either — so it froze at the old edit boundary. A frozen watermark then dropped post-watermark state.db rows whenever the sidecar was later reconstructed empty (recovery/reconcile), permanently losing the turns sent after the edit (state.db still had them). Retire a POSITIVE watermark to None once the new user turn is COMMITTED to session.messages — at the success-merge (3 sites), eager-checkpoint, error/ recovery materialization, and cold-load repair commit points. Not at chat-start: in deferred mode the new row isn't in messages yet, so a merge in that window would resurrect the replaced tail (the max-sidecar guard hasn't risen past the old boundary). Once committed, max_sidecar_timestamp rises past the replaced tail and the merge suppresses it without the watermark, so retiring is safe. Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that must keep blocking all state replay, so the clear is falsy-gated. Closes #3831 * chore(changelog): clarify watermark-retirement timing to commit-time Greptile review noted the original phrase "retires the watermark at the start of a new user turn" was timing-imprecise. The retirement actually fires when the new turn is durably committed to session.messages — at the agent-result merge, the eager user-message checkpoint, or the cold-load recovery commit. Reword for accuracy; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2) Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error) and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save), which inline the falsy-gated watermark clear instead of calling the tested helper. The error path is precisely the #3831 failure mode (recovery/reconcile after a crash), so a dedicated regression test closes that gap. Both assert a positive watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is preserved. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:39:32 +00:00
assert any(m.get("content") == "eager new turn" for m in s.messages)
s0 = _FakeSession(0.0)
routes._checkpoint_user_message_for_eager_session_save(
s0, "eager new turn", None, started_at=200.0
)
assert s0.truncation_watermark == 0.0
# --- Advanced watermark filters pre-edit state.db rows -------------------------
def test_advanced_watermark_filters_pre_edit_state_db_rows():
"""With the watermark advanced to the new message timestamp, pre-edit state.db
rows whose timestamps are below the watermark AND whose content is not in the
sidecar are still filtered out."""
sidecar = _rows(
("user", "square", 200),
("assistant", "360", 201),
)
state_db = _rows(
("user", "triangle", 100), # pre-edit, below watermark, not in sidecar
("assistant", "180", 101), # pre-edit assistant, below watermark
("user", "square", 200), # in sidecar
("assistant", "360", 201), # in sidecar
)
merged = models.merge_session_messages_append_only(
sidecar, state_db, truncation_watermark=200.0
)
contents = [m["content"] for m in merged]
assert "triangle" not in contents
assert "180" not in contents
assert "square" in contents
assert "360" in contents