fix(issue-5): record an accepted intent, not a refused one

The subscriber marked a notification intent as seen before the monotonic
sequence guard ran. A late packet was then dropped by that guard with its
intent already burned, so the genuine retry of a notification this
subscriber had never delivered was mistaken for a replay and dropped too.

Identity comparison stays ahead of the sequence guard -- a duplicate must
not consume a sequence number -- but the bookkeeping write moves behind
it, so "seen" means "accepted" rather than "arrived".

An intent that cannot yield a stable identity (not an object, or holding
an unhashable value) is now refused instead of raising out of the
callback: a paho callback that raises takes the network loop down for
every job that subscriber is watching.

Six regressions; three of them fail on the unfixed tree, and one drives
a real notifier retry through the real consumer with no hand-built
payload. Full suite: 732 passed, no skips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-07 08:11:37 +09:00
1 parent 9a651ccc71
commit 8d7e010f9f
4 files changed
+162 -5

No files matched your search

@@ -140,20 +140,43 @@ class _Watcher:
if seq is None or not isinstance(seq, int):
logger.warning("drop event for job %s: missing or invalid seq", jid)
return
intent = (payload.get("data") or {}).get("notification_intent")
if isinstance(intent, dict):
data = payload.get("data")
intent = data.get("notification_intent") if isinstance(data, dict) else None
identity = None
if intent is not None:
# Present but not an object: it cannot yield a stable identity, so the
# replay guard below could not protect it. Refuse rather than admit a
# notification we would be unable to recognise on its retry.
if not isinstance(intent, dict):
logger.warning("drop event for job %s: notification_intent is %s, not an object",
jid, type(intent).__name__)
return
identity = (intent.get("seq"), intent.get("event"), intent.get("timestamp"))
if identity in self.seen_intents.setdefault(jid, set()):
try:
already_seen = identity in self.seen_intents.setdefault(jid, set())
except TypeError:
# A malformed sender can put an unhashable value in a field. That
# must not escape defensive parsing as an exception: a callback
# raising here would take down the network loop for every job.
logger.warning("drop event for job %s: notification intent %r has no stable identity",
jid, intent)
return
if already_seen:
logger.info("drop event for job %s: notification intent %s already accepted",
jid, identity)
return
self.seen_intents[jid].add(identity)
if seq <= self.last_seq.get(jid, 0):
logger.warning("drop event for job %s: seq %d is not monotonically increasing (last %d)",
jid, seq, self.last_seq.get(jid, 0))
return
self.last_seq[jid] = seq
# Recorded only once the event is actually accepted. A late packet that the
# sequence guard refuses must not mark its intent as seen, or the genuine
# retry that follows would be mistaken for the replay of an event this
# subscriber never delivered.
if identity is not None:
self.seen_intents[jid].add(identity)
# Persistent audit log from the *subscriber's* vantage point: every event
# that survives defensive parsing is recorded here, including ones a
# different host published. This is the external-observer record that