@@ -151,13 +151,14 @@ export async function drainSessionStreamWaitpoints(
151151 * Remove a single waitpoint from the pending set. Called after a race
152152 * where `.wait()` completed the waitpoint from pre-arrived data.
153153 */
154- // "ssa" — session-stream-append. Best-effort idempotency marker for the
155- // append route: when a caller supplies an `X-Part-Id`, a retried POST
156- // whose first attempt actually committed is skipped instead of producing
157- // a duplicate record (and double-firing the waitpoint drain). The marker
158- // is only written AFTER a successful S2 append, so a retry of a genuinely
159- // failed append still goes through. 5-minute window — this covers HTTP
160- // retry storms, not a permanent idempotency store.
154+ // "ssa" — session-stream-append. Idempotency claim for the append route:
155+ // when a caller supplies an `X-Part-Id`, the first request atomically claims
156+ // the key (SET NX) before appending; a concurrent or retried POST with the
157+ // same id fails the claim and skips the append, so it never produces a
158+ // duplicate record (or double-fires the waitpoint drain). The claim is
159+ // released if the append fails, so a retry of a genuinely failed append
160+ // still goes through. 5-minute window — covers retry storms, not a
161+ // permanent idempotency store.
161162const APPEND_DEDUPE_PREFIX = "ssa:" ;
162163const APPEND_DEDUPE_TTL_SECONDS = 5 * 60 ;
163164
@@ -176,35 +177,45 @@ function buildAppendDedupeKey(
176177}
177178
178179/**
179- * True if a part with this id was already successfully appended to the
180- * channel within the dedupe window. Fails open (returns false) when Redis
181- * is unavailable — appends degrade to at-least-once, never to dropped.
180+ * Atomically claim a part id before appending. Returns true if this caller
181+ * won the claim (first to see this id) and should perform the append, false
182+ * if the id was already claimed (a concurrent or retried POST) and the append
183+ * should be skipped. Fails open (returns true) when Redis is unavailable —
184+ * appends degrade to at-least-once, never to dropped.
182185 */
183- export async function wasSessionStreamPartAppended (
186+ export async function claimSessionStreamPart (
184187 environmentId : string ,
185188 addressingKey : string ,
186189 io : "out" | "in" ,
187190 partId : string
188191) : Promise < boolean > {
189- if ( ! redis ) return false ;
192+ if ( ! redis ) return true ;
190193
191194 try {
192- const value = await redis . get ( buildAppendDedupeKey ( environmentId , addressingKey , io , partId ) ) ;
193- return value !== null ;
195+ // SET NX is the atomic claim: "OK" when set (we won), null when the key
196+ // already exists (someone else owns this id).
197+ const result = await redis . set (
198+ buildAppendDedupeKey ( environmentId , addressingKey , io , partId ) ,
199+ "1" ,
200+ "EX" ,
201+ APPEND_DEDUPE_TTL_SECONDS ,
202+ "NX"
203+ ) ;
204+ return result === "OK" ;
194205 } catch ( error ) {
195- logger . error ( "Failed to read session stream append dedupe marker " , {
206+ logger . error ( "Failed to claim session stream append part " , {
196207 environmentId,
197208 addressingKey,
198209 io,
199210 partId,
200211 error,
201212 } ) ;
202- return false ;
213+ return true ;
203214 }
204215}
205216
206- /** Record a successful append so a retried POST with the same part id is skipped . */
207- export async function markSessionStreamPartAppended (
217+ /** Release a claim so a retry can proceed — called when the append itself failed . */
218+ export async function releaseSessionStreamPart (
208219 environmentId : string ,
209220 addressingKey : string ,
210221 io : "out" | "in" ,
@@ -213,14 +224,9 @@ export async function markSessionStreamPartAppended(
213224 if ( ! redis ) return ;
214225
215226 try {
216- await redis . set (
217- buildAppendDedupeKey ( environmentId , addressingKey , io , partId ) ,
218- "1" ,
219- "EX" ,
220- APPEND_DEDUPE_TTL_SECONDS
221- ) ;
227+ await redis . del ( buildAppendDedupeKey ( environmentId , addressingKey , io , partId ) ) ;
222228 } catch ( error ) {
223- logger . error ( "Failed to write session stream append dedupe marker " , {
229+ logger . error ( "Failed to release session stream append part " , {
224230 environmentId,
225231 addressingKey,
226232 io,
0 commit comments