Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dist/src/infra/shared-profiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export function buildMentionPattern(profiles) {
* belongs to. Returns { agentId, label } or null if no match.
*/
export function resolveAgentFromAlias(alias, profiles) {
if (typeof alias !== "string" || alias.length === 0)
return null;
const lower = alias.toLowerCase();
for (const [agentId, profile] of Object.entries(profiles)) {
if (profile.mentionAliases.some(a => a.toLowerCase() === lower)) {
Expand Down
8 changes: 6 additions & 2 deletions dist/src/pipeline/webhook.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,10 @@ export async function handleLinearWebhook(api, req, res) {
mentionPattern.lastIndex = 0;
const mentionMatch = text.match(mentionPattern);
if (mentionMatch) {
const alias = mentionMatch[1];
// buildMentionPattern is global, so String#match returns full matches
// rather than capture groups. Use the first full mention consistently
// with both global and non-global patterns.
const alias = mentionMatch[0]?.replace(/^@/, "");
const resolved = resolveAgentFromAlias(alias, profiles);
if (resolved) {
api.logger.info(`AgentSession routed to ${resolved.agentId} via @${alias} mention in ${text === userMessage ? "comment" : text === sessionPrompt ? "session prompt" : "promptContext"}`);
Expand Down Expand Up @@ -673,7 +676,8 @@ export async function handleLinearWebhook(api, req, res) {
if (promptedMentionPattern && userMessage) {
const mentionMatch = userMessage.match(promptedMentionPattern);
if (mentionMatch) {
const alias = mentionMatch[1];
// A global mention regex returns full matches only, not capture groups.
const alias = mentionMatch[0]?.replace(/^@/, "");
const resolved = resolveAgentFromAlias(alias, promptedProfiles);
if (resolved) {
api.logger.info(`AgentSession prompted: routed to ${resolved.agentId} via @${alias} mention`);
Expand Down
4 changes: 4 additions & 0 deletions src/infra/shared-profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ describe("resolveAgentFromAlias", () => {
const result = resolveAgentFromAlias("anything", {});
expect(result).toBeNull();
});

it.each([undefined, null, ""])("contains malformed or missing aliases without throwing", (alias) => {
expect(resolveAgentFromAlias(alias, loadAgentProfiles())).toBeNull();
});
});

// ---------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion src/infra/shared-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ export function buildMentionPattern(profiles: Record<string, AgentProfile>): Reg
* belongs to. Returns { agentId, label } or null if no match.
*/
export function resolveAgentFromAlias(
alias: string,
alias: unknown,
profiles: Record<string, AgentProfile>,
): { agentId: string; label: string } | null {
if (typeof alias !== "string" || alias.length === 0) return null;
const lower = alias.toLowerCase();
for (const [agentId, profile] of Object.entries(profiles)) {
if (profile.mentionAliases.some(a => a.toLowerCase() === lower)) {
Expand Down
7 changes: 7 additions & 0 deletions src/pipeline/webhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,9 @@ describe("AgentSessionEvent.created full flow", () => {
});

it("routes to mentioned agent when @mention is present", async () => {
// Match the production pattern: /g makes String#match return full matches,
// not capture groups.
buildMentionPatternMock.mockReturnValue(/@(mal|mason|kaylee|eureka)/gi);
resolveAgentFromAliasMock.mockReturnValue({ agentId: "kaylee", profile: { label: "Kaylee" } });

const result = await postWebhook({
Expand All @@ -820,6 +823,7 @@ describe("AgentSessionEvent.created full flow", () => {

expect(result.status).toBe(200);
await new Promise((r) => setTimeout(r, 50));
expect(resolveAgentFromAliasMock).toHaveBeenCalledWith("kaylee", expect.any(Object));
const infoCalls = (result.api.logger.info as any).mock.calls.map((c: any[]) => c[0]);
expect(infoCalls.some((msg: string) => msg.includes("routed to kaylee"))).toBe(true);
});
Expand Down Expand Up @@ -1048,6 +1052,8 @@ describe("AgentSessionEvent.prompted full flow", () => {
});

it("routes to mentioned agent in prompted follow-up", async () => {
// AgentSession.prompted uses the same global pattern as production.
buildMentionPatternMock.mockReturnValue(/@(mal|mason|kaylee|eureka)/gi);
resolveAgentFromAliasMock.mockReturnValue({ agentId: "kaylee", profile: { label: "Kaylee" } });

const result = await postWebhook({
Expand All @@ -1063,6 +1069,7 @@ describe("AgentSessionEvent.prompted full flow", () => {

expect(result.status).toBe(200);
await new Promise((r) => setTimeout(r, 50));
expect(resolveAgentFromAliasMock).toHaveBeenCalledWith("kaylee", expect.any(Object));
const infoCalls = (result.api.logger.info as any).mock.calls.map((c: any[]) => c[0]);
expect(infoCalls.some((msg: string) => msg.includes("routed to kaylee"))).toBe(true);
});
Expand Down
8 changes: 6 additions & 2 deletions src/pipeline/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,10 @@ export async function handleLinearWebhook(
mentionPattern.lastIndex = 0;
const mentionMatch = text.match(mentionPattern);
if (mentionMatch) {
const alias = mentionMatch[1];
// buildMentionPattern is global, so String#match returns full matches
// rather than capture groups. Use the first full mention consistently
// with both global and non-global patterns.
const alias = mentionMatch[0]?.replace(/^@/, "");
const resolved = resolveAgentFromAlias(alias, profiles);
if (resolved) {
api.logger.info(`AgentSession routed to ${resolved.agentId} via @${alias} mention in ${text === userMessage ? "comment" : text === sessionPrompt ? "session prompt" : "promptContext"}`);
Expand Down Expand Up @@ -764,7 +767,8 @@ export async function handleLinearWebhook(
if (promptedMentionPattern && userMessage) {
const mentionMatch = userMessage.match(promptedMentionPattern);
if (mentionMatch) {
const alias = mentionMatch[1];
// A global mention regex returns full matches only, not capture groups.
const alias = mentionMatch[0]?.replace(/^@/, "");
const resolved = resolveAgentFromAlias(alias, promptedProfiles);
if (resolved) {
api.logger.info(`AgentSession prompted: routed to ${resolved.agentId} via @${alias} mention`);
Expand Down
Loading