Skip to content

Commit 766e1a6

Browse files
made slack bot notifier ai features more robust
1 parent e724bab commit 766e1a6

10 files changed

Lines changed: 419 additions & 107 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"packageManager": "pnpm@10.33.0",
1313
"devDependencies": {
14-
"@changesets/cli": "^2.30.0",
14+
"@changesets/cli": "2.31.0",
1515
"@types/node": "25.6.0",
1616
"esbuild": "0.28.0",
1717
"knip": "6.4.1",

packages/notifier-bot/convex/aiSummary.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { type UpdateType } from '@patch-pulse/shared';
22
import { type ReleaseEvidence } from './releaseEvidence';
3+
import { getTimeoutMs, withTimeout } from './async';
34

45
const OPENAI_API_URL = 'https://api.openai.com/v1/responses';
56
const DEFAULT_NANO_MODEL = 'gpt-5-nano';
67
const DEFAULT_MINI_MODEL = 'gpt-5-mini';
8+
const OPENAI_SUMMARY_TIMEOUT_MS = getTimeoutMs(
9+
'OPENAI_SUMMARY_TIMEOUT_MS',
10+
15_000,
11+
);
712

813
function buildSummaryPrompt(args: {
914
packageName: string;
@@ -49,44 +54,52 @@ async function callOpenAiSummary(
4954
// Caller (summarizeReleaseEvidence) guards that the key exists before calling.
5055
const apiKey = process.env.OPENAI_API_KEY!;
5156

52-
const response = await fetch(OPENAI_API_URL, {
53-
method: 'POST',
54-
headers: {
55-
'Content-Type': 'application/json',
56-
Authorization: `Bearer ${apiKey}`,
57-
},
58-
body: JSON.stringify({
59-
model,
60-
input: [
61-
{
62-
role: 'system',
63-
content: [
57+
return withTimeout(
58+
(async () => {
59+
const response = await fetch(OPENAI_API_URL, {
60+
method: 'POST',
61+
headers: {
62+
'Content-Type': 'application/json',
63+
Authorization: `Bearer ${apiKey}`,
64+
},
65+
body: JSON.stringify({
66+
model,
67+
input: [
68+
{
69+
role: 'system',
70+
content: [
71+
{
72+
type: 'input_text',
73+
text:
74+
'You summarize software releases for Slack. Use only the provided evidence. ' +
75+
'Do not speculate. Return a single plain-text sentence under 240 characters. ' +
76+
'If the evidence is insufficient, return exactly INSUFFICIENT.',
77+
},
78+
],
79+
},
6480
{
65-
type: 'input_text',
66-
text:
67-
'You summarize software releases for Slack. Use only the provided evidence. ' +
68-
'Do not speculate. Return a single plain-text sentence under 240 characters. ' +
69-
'If the evidence is insufficient, return exactly INSUFFICIENT.',
81+
role: 'user',
82+
content: [{ type: 'input_text', text: prompt }],
7083
},
7184
],
72-
},
73-
{
74-
role: 'user',
75-
content: [{ type: 'input_text', text: prompt }],
76-
},
77-
],
78-
}),
79-
});
85+
}),
86+
});
8087

81-
if (!response.ok) {
82-
throw new Error(`OpenAI API error ${response.status}`);
83-
}
88+
if (!response.ok) {
89+
throw new Error(`OpenAI API error ${response.status}`);
90+
}
8491

85-
const data = (await response.json()) as {
86-
output_text?: string;
87-
};
92+
const data = (await response.json()) as {
93+
output_text?: string;
94+
};
8895

89-
return data.output_text?.trim() || null;
96+
return data.output_text?.trim() || null;
97+
})(),
98+
{
99+
label: `OpenAI summary request (${model})`,
100+
timeoutMs: OPENAI_SUMMARY_TIMEOUT_MS,
101+
},
102+
);
90103
}
91104

92105
export async function summarizeReleaseEvidence(args: {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function getTimeoutMs(envName: string, fallbackMs: number): number {
2+
const rawValue = process.env[envName];
3+
if (!rawValue) return fallbackMs;
4+
5+
const parsedValue = Number(rawValue);
6+
return Number.isFinite(parsedValue) && parsedValue > 0
7+
? parsedValue
8+
: fallbackMs;
9+
}
10+
11+
export async function withTimeout<T>(
12+
operation: Promise<T>,
13+
args: {
14+
label: string;
15+
timeoutMs: number;
16+
},
17+
): Promise<T> {
18+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
19+
const timeoutPromise = new Promise<never>((_, reject) => {
20+
timeoutId = setTimeout(() => {
21+
reject(new Error(`${args.label} timed out after ${args.timeoutMs}ms`));
22+
}, args.timeoutMs);
23+
});
24+
25+
try {
26+
return await Promise.race([operation, timeoutPromise]);
27+
} finally {
28+
if (timeoutId) clearTimeout(timeoutId);
29+
}
30+
}

packages/notifier-bot/convex/polling.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,6 @@ export const checkForUpdates = internalAction({
8080
const version = getNpmLatestVersion(manifest);
8181
if (!version) continue;
8282

83-
await ctx.runMutation(internal.packages.touchLastChecked, {
84-
packageId: pkg._id,
85-
});
86-
8783
const { status } = getDependencyStatus({
8884
packageName: pkg.name,
8985
currentVersion: pkg.currentVersion,
@@ -165,6 +161,10 @@ export const checkForUpdates = internalAction({
165161
channelMap.set(key, entry);
166162
updatesBySubscriber.set(sub.subscriberId, channelMap);
167163
}
164+
} else {
165+
await ctx.runMutation(internal.packages.touchLastChecked, {
166+
packageId: pkg._id,
167+
});
168168
}
169169
}
170170

0 commit comments

Comments
 (0)