Skip to content

Commit fdf0d77

Browse files
committed
fix: prevent false-positive Slack auth error + add custom booking field support
Fix 1: Updated getCustomErrorMessage in all 3 handlers to return generic error when lastStreamErrorRef is set and caught error is Slack auth error. Fix 2a: Added responses?: Record<string, unknown> to CreateBookingInput and CreatePublicBookingInput types. Fix 2b: Added responses parameter to book_meeting and book_meeting_public tool schemas, passed through to API calls. Fix 2c: Included bookingFields in list_event_types_by_username response. Fix 2d: Added CUSTOM BOOKING FIELDS section to system prompt instructing agent to collect required custom field values and pass as responses. Fix 3: Hardened postAgentStream to re-throw generic error when Slack auth error is secondary to an agent stream failure. Also fixed duplicate rule #6 numbering in CRITICAL RULES.
1 parent 28a28f8 commit fdf0d77

3 files changed

Lines changed: 55 additions & 6 deletions

File tree

apps/chat/lib/agent.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ DURATION VALIDATION:
160160
"You selected a 30-minute meeting, but 10:00-10:15 is only 15 minutes. Shall I book 10:00-10:30 instead, or switch to a 15-minute event type?"
161161
- The event type duration is canonical. Use the START of the user's range as startTime.
162162
163+
CUSTOM BOOKING FIELDS:
164+
- When \`list_event_types_by_username\` returns event types with \`bookingFields\`, check for fields with \`required: true\`.
165+
- Before calling \`book_meeting_public\` (or \`book_meeting\`), ask the user for values for ALL required custom fields.
166+
- Pass the collected values as \`responses\` in the booking call. The key is the field's \`name\` (slug), the value is the user's answer.
167+
Example: if bookingFields includes \`{ name: "what-are-you-working-on", type: "text", required: true }\`, ask the user and pass \`responses: { "what-are-you-working-on": "their answer" }\`.
168+
- Non-required fields can be skipped unless the user volunteers the info.
169+
163170
MULTI-ATTENDEE:
164171
- Primary attendee goes in attendeeName/attendeeEmail of book_meeting.
165172
- Additional attendees with full details (name + timezone from [Context]): use add_booking_attendee after booking.
@@ -186,9 +193,9 @@ Do NOT automatically resume an incomplete task from earlier in the conversation.
186193
4. If check_availability returns \`totalSlots: 0\`, read the \`noSlotsReason\` and present the \`nextAvailableSlots\` as alternatives. NEVER say "I wasn't able to check" or "I couldn't check" — the check succeeded, there are just no slots for that date.
187194
5. If check_availability returns slots, USE them in your response. Do not discard results.
188195
6. NEVER call \`check_availability\` for another user's event type — it requires the host's auth token. Use \`check_availability_public\` instead (pass eventTypeSlug + username).
189-
6. Never call a tool with empty or placeholder arguments.
190-
7. During a booking flow, sequential tool calls across steps are expected (list_event_types → check_availability → book_meeting). After completing the task, respond with text.
191-
8. NEVER call create_event_type, update_event_type, or delete_event_type unless the user explicitly asked to create/update/delete an event type.
196+
7. Never call a tool with empty or placeholder arguments.
197+
8. During a booking flow, sequential tool calls across steps are expected (list_event_types → check_availability → book_meeting). After completing the task, respond with text.
198+
9. NEVER call create_event_type, update_event_type, or delete_event_type unless the user explicitly asked to create/update/delete an event type.
192199
193200
## Formatting Rules
194201
${
@@ -341,6 +348,7 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
341348
description: et.description,
342349
hidden: et.hidden,
343350
bookingUrl: et.bookingUrl,
351+
bookingFields: et.bookingFields,
344352
})),
345353
};
346354
} catch (err) {
@@ -579,6 +587,13 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
579587
"Email addresses of additional attendees (email-only). Use when you have emails but not full details for extra guests."
580588
),
581589
notes: z.string().nullable().optional().describe("Optional notes for the booking"),
590+
responses: z
591+
.record(z.string(), z.unknown())
592+
.nullable()
593+
.optional()
594+
.describe(
595+
"Custom booking field responses. Keys are field slugs from the event type's bookingFields, values are the user's answers. Required when the event type has required custom fields."
596+
),
582597
}),
583598
execute: async ({
584599
eventTypeId,
@@ -588,6 +603,7 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
588603
attendeeTimeZone,
589604
guestEmails,
590605
notes,
606+
responses,
591607
}) => {
592608
const token = await getAccessTokenOrNull(teamId, userId);
593609
if (!token) return { error: "Account not connected." };
@@ -604,6 +620,7 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
604620
},
605621
guests: guestEmails?.filter(Boolean) ?? undefined,
606622
notes: notes ?? undefined,
623+
responses: responses ?? undefined,
607624
});
608625

609626
return {
@@ -657,6 +674,13 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
657674
.nullable()
658675
.optional()
659676
.describe("Duration in minutes. Only needed if the event type supports multiple durations."),
677+
responses: z
678+
.record(z.string(), z.unknown())
679+
.nullable()
680+
.optional()
681+
.describe(
682+
"Custom booking field responses. Keys are field slugs from the event type's bookingFields, values are the user's answers. Required when the event type has required custom fields."
683+
),
660684
}),
661685
execute: async ({
662686
eventTypeSlug,
@@ -668,6 +692,7 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
668692
guests,
669693
notes,
670694
lengthInMinutes,
695+
responses,
671696
}) => {
672697
const linked = await getLinkedUser(teamId, userId);
673698

@@ -684,6 +709,7 @@ function createCalTools(teamId: string, userId: string, lookupPlatformUser?: Loo
684709
guests: guests?.filter(Boolean) ?? undefined,
685710
notes: notes ?? undefined,
686711
lengthInMinutes: lengthInMinutes ?? undefined,
712+
responses: responses ?? undefined,
687713
});
688714

689715
return {

apps/chat/lib/bot.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,18 @@ async function postAgentStream(
727727
userId: ctx.userId,
728728
threadId: thread.id,
729729
});
730+
// If the thrown error is a Slack auth error but the agent stream itself had
731+
// issues (captured via onErrorRef), the Slack error is a secondary failure
732+
// caused by AsyncLocalStorage context loss — not a real token problem.
733+
// Re-throw as a generic error so withBotErrorHandling doesn't misclassify it.
734+
if (isSlackAuthError(err) && options?.onErrorRef?.current) {
735+
log.warn("Slack auth error is secondary to stream error, re-throwing as generic", {
736+
streamError: options.onErrorRef.current.message,
737+
});
738+
throw new Error(
739+
`Agent stream failed: ${options.onErrorRef.current.message}`
740+
);
741+
}
730742
throw err;
731743
}
732744
}
@@ -819,12 +831,15 @@ bot.onNewMessage(/[\s\S]+/, async (thread, message) => {
819831
{
820832
postError: (msg) => thread.post(msg).catch(() => {}),
821833
logContext: "telegram freeform",
822-
getCustomErrorMessage: () => {
834+
getCustomErrorMessage: (err) => {
823835
if (!lastStreamErrorRef.current) return undefined;
824836
if (isAIRateLimitError(lastStreamErrorRef.current))
825837
return "I've hit my daily token limit. Please try again later when the limit resets.";
826838
if (isAIToolCallError(lastStreamErrorRef.current))
827839
return "I had trouble processing that request. Please try again, or be more specific (e.g. run /cal bookings first, then cancel by booking ID).";
840+
// Any other stream error should NOT fall through to Slack auth check
841+
if (isSlackAuthError(err))
842+
return "Sorry, something went wrong while processing your request. Please try again.";
828843
return undefined;
829844
},
830845
}
@@ -939,12 +954,15 @@ bot.onNewMention(async (thread, message) => {
939954
{
940955
postError: safePost,
941956
logContext: "handling mention",
942-
getCustomErrorMessage: () => {
957+
getCustomErrorMessage: (err) => {
943958
if (!lastStreamErrorRef.current) return undefined;
944959
if (isAIRateLimitError(lastStreamErrorRef.current))
945960
return "I've hit my daily token limit. Please try again later when the limit resets.";
946961
if (isAIToolCallError(lastStreamErrorRef.current))
947962
return "I had trouble processing that request. Please try again, or be more specific (e.g. run /cal bookings first, then cancel by booking ID).";
963+
// Any other stream error should NOT fall through to Slack auth check
964+
if (isSlackAuthError(err))
965+
return "Sorry, something went wrong while processing your request. Please try again.";
948966
return undefined;
949967
},
950968
}
@@ -1046,12 +1064,15 @@ bot.onSubscribedMessage(async (thread, message) => {
10461064
{
10471065
postError: safePost,
10481066
logContext: "thread follow-up",
1049-
getCustomErrorMessage: () => {
1067+
getCustomErrorMessage: (err) => {
10501068
if (!lastStreamErrorRef.current) return undefined;
10511069
if (isAIRateLimitError(lastStreamErrorRef.current))
10521070
return "I've hit my daily token limit. Please try again later when the limit resets.";
10531071
if (isAIToolCallError(lastStreamErrorRef.current))
10541072
return "I had trouble processing that request. Please try again, or be more specific (e.g. run /cal bookings first, then cancel by booking ID).";
1073+
// Any other stream error should NOT fall through to Slack auth check
1074+
if (isSlackAuthError(err))
1075+
return "Sorry, something went wrong while processing your request. Please try again.";
10551076
return undefined;
10561077
},
10571078
}

apps/chat/lib/calcom/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export interface CreateBookingInput {
8585
guests?: string[];
8686
notes?: string;
8787
metadata?: Record<string, string>;
88+
responses?: Record<string, unknown>;
8889
}
8990

9091
export interface CreatePublicBookingInput {
@@ -99,6 +100,7 @@ export interface CreatePublicBookingInput {
99100
guests?: string[];
100101
notes?: string;
101102
lengthInMinutes?: number;
103+
responses?: Record<string, unknown>;
102104
}
103105

104106
/** Metadata passed from Cal.com booking form for routing notifications. */

0 commit comments

Comments
 (0)