feat(chat): client turn-id makes fire-and-forget bulletproof
Brian fires a quick message then locks his phone to go play the hand — which drops the SSE stream, and on wake the UI re-fires via the blocking fallback. The prior (session, message) + 20s window caught the near-simultaneous case but not a re-fire minutes later. Now the UI stamps each send with a unique turnId (crypto.randomUUID) and carries the SAME id on both the stream and the fallback; the server dedupes on it. Bulletproof regardless of how long he's away — lock for an hour, come back, still exactly one execution and one log — and a genuinely new send gets a fresh id so nothing legit is swallowed. Id-keyed turns keep a long (1h) window; requests without an id keep the short (session, msg) window for near-simultaneous dupes. Verified end-to-end: two POSTs with the same turnId → one reply, one persisted exchange pair (the duplicate reused the owner's result). 9 dedup tests; suite 235. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,30 @@ def test_fresh_message_after_window_runs_again():
|
||||
# a completed turn lingers only briefly; simulate expiry and confirm re-ownership
|
||||
o1, rec = chat._claim_turn("s1", "later resend")
|
||||
chat._finish_turn(rec, "first")
|
||||
rec["ts"] -= chat._TURN_TTL + 1 # age it past the window
|
||||
rec["ts"] -= chat._TURN_TTL_MSG + 1 # age it past the (session,msg) window
|
||||
o2, _ = chat._claim_turn("s1", "later resend")
|
||||
assert o1 and o2 # a genuine later resend runs fresh
|
||||
|
||||
|
||||
# --- client turn-id keying (the fire-and-forget guarantee) ----------------
|
||||
|
||||
def test_same_turn_id_dedupes_regardless_of_message():
|
||||
# the fallback may resend the SAME id; dedupe on the id, not the text
|
||||
o1, r1 = chat._claim_turn("s1", "a hand", turn_id="tid-123")
|
||||
o2, r2 = chat._claim_turn("s1", "a hand", turn_id="tid-123")
|
||||
assert o1 is True and o2 is False and r1 is r2
|
||||
|
||||
|
||||
def test_different_turn_ids_each_own():
|
||||
o1, _ = chat._claim_turn("s1", "same text", turn_id="tid-1")
|
||||
o2, _ = chat._claim_turn("s1", "same text", turn_id="tid-2")
|
||||
assert o1 and o2 # a genuinely new send never gets swallowed
|
||||
|
||||
|
||||
def test_turn_id_window_survives_long_after_the_msg_window():
|
||||
# locked-phone case: the re-fire can arrive minutes later and must still dedupe
|
||||
o1, rec = chat._claim_turn("s1", "big hand", turn_id="tid-lock")
|
||||
chat._finish_turn(rec, "cached")
|
||||
rec["ts"] -= chat._TURN_TTL_MSG + 60 # well past the short window, but not the id window
|
||||
o2, r2 = chat._claim_turn("s1", "big hand", turn_id="tid-lock")
|
||||
assert o1 and o2 is False and r2["reply"] == "cached"
|
||||
|
||||
Reference in New Issue
Block a user