Skip to content

Commit d817ab8

Browse files
committed
fix(orchestration): bot must not trigger on its OWN comments — kill the self-reply loop (aws-samples#247 UX.20 root cause)
The 50-reply spam was NOT webhook redelivery (my first UX.20 theory) — it was a genuine INFINITE SELF-TRIGGER LOOP: the parent-epic disambiguation reply embeds a literal example '@bgagent ABCA-123: <what to change>'. That reply is itself a Linear comment → fires a Comment webhook → parseCommentTrigger's /@bgagent/ regex matched the mention INSIDE the bot's own reply → posted another disambiguation reply (also containing @bgagent) → … ~50 deep. Each iteration was a NEW comment with a NEW id, so the per-comment ack-claim (UX.20 round 1) couldn't dedup it — it guarded redelivery, not self-reference. Root fix (the guard the Linear path never had — Slack already skips its own bot_id messages to avoid exactly this): parseCommentTrigger now returns NOT-triggered for any comment whose trimmed body starts with one of the bot's own template markers (👋 ✅ ❌ ⚠️ 🔄 🤖 🖼️ 🔗) — exported isBotAuthoredComment. The bot can never act on a comment it authored, regardless of what example text the body contains. The pre-existing A6 trigger never looped only because the agent's progress comments happen not to contain @bgagent; UX.18's reply was the first bot comment that did. The UX.20-round-1 ack-claim (claimCommentAck) stays — it's still correct defense against genuine webhook redelivery; this is the orthogonal, primary fix for self-reference. Tests: the EXACT spam body (👋 + embedded @bgagent example) → not triggered; every template prefix → bot-authored; a real human @bgagent → still triggers; leading-whitespace marker still caught. 41 green across trigger+webhook suites.
1 parent e44cd43 commit d817ab8

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

cdk/src/handlers/shared/orchestration-comment-trigger.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ export interface CommentTrigger {
5959
*/
6060
export function parseCommentTrigger(body: string | undefined | null): CommentTrigger {
6161
if (!body) return { triggered: false, instruction: '' };
62+
// SELF-COMMENT GUARD (#247 UX.20 — live-caught infinite loop): the bot's OWN
63+
// rendered comments must NEVER trigger it, or it talks to itself forever.
64+
// This bit me when the disambiguation reply embedded a literal "@bgagent
65+
// ABCA-123: …" example — the reply re-matched the mention and spawned another
66+
// reply, ~50 deep. The agent's progress comments are also bot-authored.
67+
// Cheapest robust signal that needs no actor-identity config: a body that
68+
// STARTS WITH one of our own template markers is ours, not a user
69+
// instruction. (Linear strips a leading emoji to its own line sometimes, so
70+
// we test the trimmed start.) Keep this list in sync with the rendered
71+
// comment prefixes (panel, acks, disambiguation, agent progress).
72+
if (isBotAuthoredComment(body)) return { triggered: false, instruction: '' };
6273
// Token-boundary match: @bgagent not immediately followed by a word char or
6374
// a '.' (so it won't fire on @bgagentbot or an @bgagent.io address).
6475
const re = /@bgagent(?![\w.])/gi;
@@ -67,6 +78,28 @@ export function parseCommentTrigger(body: string | undefined | null): CommentTri
6778
return { triggered: true, instruction };
6879
}
6980

81+
/**
82+
* Markers that begin a comment the BOT itself rendered (panel, acks,
83+
* disambiguation reply, agent progress). A comment starting with any of these
84+
* is never a human instruction — used to break self-trigger loops (#247 UX.20).
85+
*/
86+
const BOT_COMMENT_PREFIXES = [
87+
'👋', // disambiguation "which sub-issue?" reply
88+
'✅', // "✅ Updated — PR #…" ack / "✅ **ABCA orchestration complete**" panel
89+
'❌', // failure reply
90+
'⚠️', // "finished with failures" panel
91+
'🔄', // in-progress panel
92+
'🤖', // agent progress ("🤖 Starting…")
93+
'🖼️', // preview screenshot comment
94+
'🔗', // "PR opened" / combined-PR
95+
] as const;
96+
97+
/** True when ``body`` is one of the bot's own rendered comments (loop guard). */
98+
export function isBotAuthoredComment(body: string): boolean {
99+
const trimmed = body.trimStart();
100+
return BOT_COMMENT_PREFIXES.some((p) => trimmed.startsWith(p));
101+
}
102+
70103
/**
71104
* Build the task description handed to ``coding/pr-iteration-v1`` from the
72105
* comment instruction. When the reviewer left explicit text, that IS the

cdk/test/handlers/shared/orchestration-comment-trigger.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import {
2121
buildIterationInstruction,
22+
isBotAuthoredComment,
2223
parseCommentTrigger,
2324
} from '../../../src/handlers/shared/orchestration-comment-trigger';
2425

@@ -70,6 +71,45 @@ describe('parseCommentTrigger', () => {
7071
expect(t.triggered).toBe(true);
7172
expect(t.instruction).toBe('do X and also Y');
7273
});
74+
75+
// #247 UX.20 — the self-trigger infinite loop. The bot's OWN comments must
76+
// never re-trigger it, even when (esp. when) they contain a literal @bgagent.
77+
describe('self-comment guard (#247 UX.20 loop prevention)', () => {
78+
test('the disambiguation reply does NOT trigger, even though it embeds "@bgagent ABCA-123:"', () => {
79+
// This EXACT body spammed ~50 replies live: it starts with 👋 and contains
80+
// a literal @bgagent example, which the old regex re-matched → loop.
81+
const body = '👋 I couldn\'t tell which sub-issue that\'s about.\n\nOtherwise, comment on the '
82+
+ 'specific sub-issue, or name it here — e.g. `@bgagent ABCA-123: <what to change>`. The sub-issues are:';
83+
expect(parseCommentTrigger(body).triggered).toBe(false);
84+
expect(isBotAuthoredComment(body)).toBe(true);
85+
});
86+
87+
test('all bot template prefixes are recognized as bot-authored (never trigger)', () => {
88+
for (const body of [
89+
'👋 That could apply to more than one sub-issue…',
90+
'✅ Updated — PR #193.',
91+
'✅ **ABCA orchestration complete**',
92+
'❌ I made the change, but the build/tests didn\'t pass.',
93+
'⚠️ **ABCA orchestration finished with failures**',
94+
'🔄 **ABCA orchestration** · 1/3 complete',
95+
'🤖 Starting on this issue…',
96+
'🖼️ **Preview screenshot**',
97+
'🔗 PR opened: https://github.com/o/r/pull/9',
98+
]) {
99+
expect(isBotAuthoredComment(body)).toBe(true);
100+
expect(parseCommentTrigger(body).triggered).toBe(false);
101+
}
102+
});
103+
104+
test('a genuine human @bgagent comment is NOT misclassified as bot-authored', () => {
105+
expect(isBotAuthoredComment('@bgagent for the footer change the tagline')).toBe(false);
106+
expect(parseCommentTrigger('@bgagent for the footer change the tagline').triggered).toBe(true);
107+
});
108+
109+
test('leading whitespace before a bot marker is still caught', () => {
110+
expect(isBotAuthoredComment(' \n✅ Updated — PR #193.')).toBe(true);
111+
});
112+
});
73113
});
74114

75115
describe('buildIterationInstruction', () => {

0 commit comments

Comments
 (0)