diff --git a/tests/persona-matrix-mouth.test.mjs b/tests/persona-matrix-mouth.test.mjs index bc9f71d..958b684 100644 --- a/tests/persona-matrix-mouth.test.mjs +++ b/tests/persona-matrix-mouth.test.mjs @@ -2124,3 +2124,9 @@ test("display name: a sender with an empty localpart is delivered under its own assert.equal(rounds[0][0].senderDisplay, "@:evil.test"); assert.doesNotThrow(() => docs.pendingV4Tuple({ persona: "p", deliveries: rounds[0] })); }); + +test("round cap: a pin serves only the event whose check made it: a later event's target past its 60 s keep is read again, as counted", async () => { + const { rounds, gets } = await capRun([quoting("$e1", "$x"), quoting("$e2", "$r"), quoting("$e3", "$x")], { lapseOnR: 2000 }); + assert.deepEqual(rounds.flat().map(d => d.eventId), ["$e1", "$e2", "$e3"]); + assert.equal(gets("$x"), 2, "the read at t0 and one re-read for $e3"); +}); diff --git a/tools/persona-matrix-mouth.mjs b/tools/persona-matrix-mouth.mjs index db04fee..3f71552 100644 --- a/tools/persona-matrix-mouth.mjs +++ b/tools/persona-matrix-mouth.mjs @@ -317,7 +317,7 @@ export function createMouth({ const REPLY_READS_PER_ROUND = 8; // target GETs one sync round may make, whoever asks (basis or reply_to): an event whose uncached targets would pass it waits for the next round const KEEP_HIT_MS = 10 * 60 * 1000; // how long transient failures may keep a hit's sender alive after its last good read let replyReads = 0; - const held = new Map(); // key -> entry: targets the round's cap check counted as cached, kept for the round so the 60 s keep cannot lapse between the check and the read and spend a GET the check did not count + const held = new Map(); // key -> entry: targets the round's cap check counted as cached, kept for that event so the 60 s keep cannot lapse between the check and the read and spend a GET the check did not count const cachedTarget = key => { const known = authors.get(key); if (!known) return null; @@ -382,6 +382,7 @@ export function createMouth({ const relates = event.content["m.relates_to"]; if (!replyLink || formOf(event.content) !== "message") return 0; const ids = new Set([relates?.["m.in_reply_to"]?.event_id, relates?.rel_type === "m.thread" ? relates.event_id : undefined].filter(id => typeof id === "string" && id !== "")); + held.clear(); // events run one at a time: a pin serves only the event whose check made it let reads = 0; for (const id of ids) { const key = `${roomId} ${id}`, hit = cachedTarget(key);