From b0cb9376f49d926058d588c17bf4b6a2c337919b Mon Sep 17 00:00:00 2001 From: Niladri Das <125604915+bniladridas@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:26:43 +0530 Subject: [PATCH 1/2] fix(agents): clear the first-run flag when the catch-up completes The flag says this process began with no earlier position, so an unrecognised event older than the start cutoff was replayed history rather than something missed. Only the backlog push cleared it, and that returns early when nothing was skipped, so a quiet first run kept the flag for the life of the process. After a reconnect, the replay can hold an event from before the session, which was missed and owes a resync. Left set, the flag took it for history and dropped it without a trace, and nothing told the agent it had lost a section of the stream. Clear it in the catch-up that ends the replay, whether or not anything was skipped, so the flag describes the first replay and no later one. Signed-off-by: Niladri Das <125604915+bniladridas@users.noreply.github.com> --- src/pepper_cli/claude_channel.py | 4 +++- tests/test_claude_channel.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/pepper_cli/claude_channel.py b/src/pepper_cli/claude_channel.py index 2e117f2..e9e1262 100644 --- a/src/pepper_cli/claude_channel.py +++ b/src/pepper_cli/claude_channel.py @@ -499,7 +499,6 @@ def _flush_backlog(self) -> None: self._push(content, meta) self.skipped.clear() self.skipped_times.clear() - self.first_run = False def _live_edge(self) -> None: """Tell Claude, once, that the channel started at the live edge: there is no backlog to @@ -549,6 +548,9 @@ def _topic_words(self, topic_id: str) -> str: def _caught_up(self) -> None: self._flush_backlog() self._flush_resync() + # The catch-up is over whether or not anything was skipped. Left set when nothing was, + # ``first_run`` would go on suppressing resyncs for the rest of the process. + self.first_run = False self._persist() def _resync(self, cause: str) -> None: diff --git a/tests/test_claude_channel.py b/tests/test_claude_channel.py index ad7032f..bbc593b 100644 --- a/tests/test_claude_channel.py +++ b/tests/test_claude_channel.py @@ -888,6 +888,23 @@ def test_a_resync_names_its_cause_gives_room_ids_and_is_safe_on_a_first_run(monk assert f"{_TOPIC} (room_id {_ROOM}; Kitchen › Dinner)" in note["content"] +def test_an_old_event_replayed_after_a_first_run_still_owes_a_resync(monkeypatch): + """A first run whose window holds no messages pushes no backlog, so nothing but the completed + catch-up can clear ``first_run``. The stream then drops, and the reconnect replays an older + unrecognised event, which was missed and owes a resync. Left set, the flag takes it for + first-run history and drops it.""" + old = _now() - timedelta(days=3) + fake = _Fake( + _CONNECTED + _scope() + _PING, + 503, + _CONNECTED + _scope() + _event(7, kind="group.renamed", created=old) + _PING, + ) + + result = run_channel(monkeypatch, fake, ["--room", _ROOM, "--start", "beginning"]) + + assert result.kinds() == ["resync"] + + def test_a_first_run_replay_of_old_room_events_owes_no_resync(monkeypatch): """The retained window on a first run holds old room events of every kind (a rename, an app install); none was missed, and the backlog already says to catch up.""" From 3dd17caf37574a5fd93d2cb2f6f6f3e1ac1dc534 Mon Sep 17 00:00:00 2001 From: Niladri Das <125604915+bniladridas@users.noreply.github.com> Date: Sat, 14 Nov 2026 08:44:01 +0530 Subject: [PATCH 2/2] fix(agents): end the catch-up on a ping, not on any other comment The channel treated every comment except connected as proof the replay was over. The contract defines two: connected opens a connection, and a ping is sent only while the server has nothing past the caller's position, so the ping is the replay boundary. Any other comment, a proxy keepalive or a frame from a newer server, says neither. The early flush matters most on a first run: an unrelated comment cleared first_run before the replay ended, so an old event replayed after it owed a spurious resync and the backlog lost its first-run wording. The new test plays a keepalive before the drop and an old renamed event after the reconnect, and fails on the old check. Refs #122 Signed-off-by: Niladri Das <125604915+bniladridas@users.noreply.github.com> --- src/pepper_cli/claude_channel.py | 5 ++++- tests/test_claude_channel.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/pepper_cli/claude_channel.py b/src/pepper_cli/claude_channel.py index e9e1262..4694d3a 100644 --- a/src/pepper_cli/claude_channel.py +++ b/src/pepper_cli/claude_channel.py @@ -831,7 +831,10 @@ def _handle_frame(self, frame: api_client.FeedFrame) -> str | None: if frame.kind == "error": return self._frame_problem(frame) if frame.kind == "comment": - if frame.payload.get("comment") != "connected": + # ``: ping`` is the contract's replay boundary: the server sends one only while it + # has nothing past this position. ``: connected`` opens a connection, and any other + # comment (a proxy keepalive, a newer server's frame) says neither has happened. + if frame.payload.get("comment") == "ping": self._caught_up() return None scoped = self.config.room is not None diff --git a/tests/test_claude_channel.py b/tests/test_claude_channel.py index bbc593b..2e04fb3 100644 --- a/tests/test_claude_channel.py +++ b/tests/test_claude_channel.py @@ -905,6 +905,26 @@ def test_an_old_event_replayed_after_a_first_run_still_owes_a_resync(monkeypatch assert result.kinds() == ["resync"] +def test_a_non_ping_comment_does_not_end_the_first_run_catch_up(monkeypatch): + """Only ``: ping`` marks replay completion; ``: connected`` opens the connection, and any + other comment (a proxy keepalive, a newer server's frame) proves neither. Left unfiltered, + such a comment ends the catch-up early, clears ``first_run``, flushes the backlog it owed, + and makes an old event replayed after it owe a spurious resync.""" + old = _now() - timedelta(days=3) + fake = _Fake( + _CONNECTED + _scope() + _event(3, created=old) + ": keepalive\n\n", + 503, + _CONNECTED + _scope() + _event(7, kind="group.renamed", created=old) + _PING, + ) + + result = run_channel(monkeypatch, fake, ["--room", _ROOM, "--start", "beginning"]) + + assert result.kinds() == ["backlog"] + (note,) = result.notifications + assert note["meta"]["first_run"] == "true" + assert "live_edge" not in note["meta"] + + def test_a_first_run_replay_of_old_room_events_owes_no_resync(monkeypatch): """The retained window on a first run holds old room events of every kind (a rename, an app install); none was missed, and the backlog already says to catch up."""