Skip to content

Commit 19bceed

Browse files
committed
fix: record cross-turn dedup synchronously before async send
Move recordDeliveredText() from the async post-delivery callback in emitBlockReplySafely to the synchronous path in emitBlockChunk, before the Telegram send. This closes the race window where context compaction could trigger a new assistant turn while the delivery is still in-flight, bypassing the dedup cache entirely. Trade-off: if the send fails transiently the text remains in the cache, but the 1-hour TTL ensures it won't suppress the same content forever. This matches the synchronous recording already done in pushAssistantText. Also fixes the hash comment: Math.imul + >>> 0 produce a 32-bit hash, not 53-bit. Addresses review feedback from greptile-apps.
1 parent c1e5ad4 commit 19bceed

File tree

2 files changed

+9
-12
lines changed

2 files changed

+9
-12
lines changed

src/agents/pi-embedded-helpers/messaging-dedupe.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type RecentDeliveredEntry = {
2323
* Build a collision-resistant hash from the full normalised text of a
2424
* delivered assistant message. Uses a fast non-cryptographic approach:
2525
* the first 200 normalised chars (for quick prefix screening) combined
26-
* with the total length and a simple 53-bit numeric hash of the full
26+
* with the total length and a simple 32-bit numeric hash of the full
2727
* string. This avoids false positives when two responses share the same
2828
* opening paragraph but diverge later.
2929
*/
@@ -32,7 +32,7 @@ export function buildDeliveredTextHash(text: string): string {
3232
if (normalized.length <= 200) {
3333
return normalized;
3434
}
35-
// 53-bit FNV-1a-inspired hash (fits in a JS safe integer).
35+
// 32-bit FNV-1a-inspired hash (Math.imul + >>> 0 operate on 32-bit integers).
3636
let h = 0x811c9dc5;
3737
for (let i = 0; i < normalized.length; i++) {
3838
h ^= normalized.charCodeAt(i);

src/agents/pi-embedded-subscribe.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,6 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
113113
}
114114
void Promise.resolve()
115115
.then(() => params.onBlockReply?.(payload))
116-
.then(() => {
117-
// Record in cross-turn dedup cache only after successful delivery.
118-
// Recording before send would suppress retries on transient failures.
119-
if (opts?.sourceText) {
120-
recordDeliveredText(opts.sourceText, state.recentDeliveredTexts);
121-
}
122-
})
123116
.catch((err) => {
124117
log.warn(`block reply callback failed: ${String(err)}`);
125118
});
@@ -524,10 +517,14 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
524517
state.lastBlockReplyText = chunk;
525518
assistantTexts.push(chunk);
526519
rememberAssistantText(chunk);
520+
// Record in cross-turn dedup cache synchronously — before the async
521+
// delivery — to close the race window where context compaction could
522+
// trigger a new turn while the Telegram send is still in-flight.
523+
// This matches the synchronous recording in pushAssistantText.
524+
// Trade-off: if the send fails transiently the text stays in the cache,
525+
// but the 1-hour TTL ensures it won't suppress the same text forever.
526+
recordDeliveredText(chunk, state.recentDeliveredTexts);
527527
if (!params.onBlockReply) {
528-
// No block reply callback — text is accumulated for final delivery.
529-
// Record now since there's no async send that could fail.
530-
recordDeliveredText(chunk, state.recentDeliveredTexts);
531528
return;
532529
}
533530
const splitResult = replyDirectiveAccumulator.consume(chunk);

0 commit comments

Comments
 (0)