Skip to content

Commit 5f9ccbe

Browse files
Simon-XYDTclaude
andcommitted
fix(auto-reply): tighten custom token prefix matching to avoid false suppression
For custom tokens with non-letter characters (e.g., HELP-QUIET), require the streamed prefix to include at least one of those non-letter characters before marking it as silent. Pure-letter prefixes like "HE" for "HELP-QUIET" can no longer suppress natural language text that happens to share the prefix. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 2e748a9 commit 5f9ccbe

2 files changed

Lines changed: 20 additions & 8 deletions

File tree

src/auto-reply/tokens.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,18 +284,27 @@ describe("isSilentReplyPrefixText", () => {
284284

285285
it("matches custom tokens with digits", () => {
286286
expect(isSilentReplyPrefixText("NOREPLY2", "NOREPLY2")).toBe(true);
287-
expect(isSilentReplyPrefixText("NORE", "NOREPLY2")).toBe(true);
288-
expect(isSilentReplyPrefixText("NOR", "NOREPLY2")).toBe(true);
287+
expect(isSilentReplyPrefixText("NOREPLY", "NOREPLY2")).toBe(false);
288+
expect(isSilentReplyPrefixText("NORE", "NOREPLY2")).toBe(false);
289+
expect(isSilentReplyPrefixText("NOR", "NOREPLY2")).toBe(false);
289290
});
290291

291292
it("matches custom tokens with hyphens", () => {
292293
expect(isSilentReplyPrefixText("NO-ANSWER", "NO-ANSWER")).toBe(true);
293294
expect(isSilentReplyPrefixText("NO-AN", "NO-ANSWER")).toBe(true);
295+
expect(isSilentReplyPrefixText("NO-", "NO-ANSWER")).toBe(true);
294296
});
295297

296298
it("rejects non-matching prefixes for custom tokens", () => {
297299
expect(isSilentReplyPrefixText("HE", "NOREPLY2")).toBe(false);
298300
expect(isSilentReplyPrefixText("HELLO", "NO-ANSWER")).toBe(false);
299301
expect(isSilentReplyPrefixText("NO-", "NOREPLY2")).toBe(false);
300302
});
303+
304+
it("rejects pure-letter prefixes for punctuated tokens to avoid false suppression", () => {
305+
expect(isSilentReplyPrefixText("HE", "HELP-QUIET")).toBe(false);
306+
expect(isSilentReplyPrefixText("HELP", "HELP-QUIET")).toBe(false);
307+
expect(isSilentReplyPrefixText("HELP-", "HELP-QUIET")).toBe(true);
308+
expect(isSilentReplyPrefixText("HELP-QUIET", "HELP-QUIET")).toBe(true);
309+
});
301310
});

src/auto-reply/tokens.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,16 @@ export function isSilentReplyPrefixText(
308308
if (normalized.includes("_")) {
309309
return true;
310310
}
311-
// For tokens without underscore, only allow bare "NO" as a partial prefix
312-
// for NO_REPLY (NO_REPLY streaming can transiently emit that fragment).
313-
// Custom tokens containing non-letter characters (digits, hyphens) are safe
314-
// for partial prefix matching because their prefixes won't match natural
315-
// language words. Full-token match is also safe for any token.
316-
if (normalized === tokenUpper || /[^A-Z_]/.test(tokenUpper)) {
311+
// Full-token match is safe for any token.
312+
if (normalized === tokenUpper) {
317313
return true;
318314
}
315+
// For custom tokens containing non-letter characters (digits, hyphens),
316+
// only match if the prefix includes at least one non-letter character
317+
// from the token. Otherwise, a pure-letter prefix like "HE" for "HELP-QUIET"
318+
// would suppress natural language that happens to share that prefix (#100007).
319+
if (/[^A-Z_]/.test(tokenUpper)) {
320+
return [...tokenUpper].some((ch) => /[^A-Z_]/.test(ch) && normalized.includes(ch));
321+
}
319322
return tokenUpper === SILENT_REPLY_TOKEN && normalized === "NO";
320323
}

0 commit comments

Comments
 (0)