filter only useful slack messages in homr

This commit is contained in:
Arjun 2026-06-04 23:54:10 +05:30
parent a22635126a
commit a4a76dd4c5
2 changed files with 86 additions and 19 deletions

View file

@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';
import { filterSlackHomeCandidatesForRelevance, SlackHomeRankCandidate } from './rank_slack_home.js';
function slackTs(dateMs: number): string {
return `${Math.floor(dateMs / 1000)}.000000`;
}
describe('Slack Home ranking filters', () => {
it('drops stale routine standup logistics but keeps durable updates', () => {
const now = Date.parse('2026-06-04T18:00:00Z');
const nineHoursAgo = now - 9 * 60 * 60 * 1000;
const twelveHoursAgo = now - 12 * 60 * 60 * 1000;
const thirtyMinutesAgo = now - 30 * 60 * 1000;
const candidates: SlackHomeRankCandidate[] = [
{
id: 'stale-standup-schedule',
channelName: 'general',
text: 'standup at 4pm possible?',
ts: slackTs(nineHoursAgo),
},
{
id: 'stale-standup-sick',
channelName: 'general',
text: 'ill skip todays standup I am having stomach ache and not feeling well',
ts: slackTs(twelveHoursAgo),
},
{
id: 'durable-issue-update',
channelName: 'general',
text: 'is the icon issue fixed for windows?',
ts: slackTs(twelveHoursAgo),
},
{
id: 'recent-standup-schedule',
channelName: 'general',
text: 'standup at 4pm possible?',
ts: slackTs(thirtyMinutesAgo),
},
];
expect(filterSlackHomeCandidatesForRelevance(candidates, now).map(candidate => candidate.id)).toEqual([
'durable-issue-update',
'recent-standup-schedule',
]);
});
});

View file

@ -22,6 +22,11 @@ const RankedSlackMessagesSchema = z.object({
rankedIds: z.array(z.string()).describe('Message ids in the order they should appear on Home.'),
});
const EXPIRED_ROUTINE_AGE_MS = 2 * 60 * 60 * 1000;
const ROUTINE_EVENT_RE = /\b(stand[-\s]?up|daily\s+(sync|scrum|standup)|scrum|check[-\s]?in)\b/i;
const ROUTINE_LOGISTICS_RE = /\b(skip|skipping|miss|missing|can't|cannot|cant|won't|wont|join|attend|possible|move|reschedule|shift|late|running\s+late|stomach|sick|not\s+feeling|headache|doctor|appointment|today|todays|today's|tomorrow|at\s+\d{1,2}(:\d{2})?\s*(am|pm)?)\b/i;
const DURABLE_SIGNAL_RE = /\b(blocker|blocked|decision|decided|owner|deadline|shipped|fixed|done|launched|deployed|merged|bug|issue|incident|outage|customer|contract|pricing|proposal|launch|release|handoff|review|approval|approved)\b/i;
function timeRank(candidates: SlackHomeRankCandidate[], limit: number): string[] {
return [...candidates]
.sort((a, b) => Number(b.ts) - Number(a.ts))
@ -29,6 +34,31 @@ function timeRank(candidates: SlackHomeRankCandidate[], limit: number): string[]
.map(candidate => candidate.id);
}
function slackTsToMs(ts: string): number | null {
const seconds = Number(ts.split('.')[0]);
if (!Number.isFinite(seconds)) return null;
return seconds * 1000;
}
function isExpiredRoutineLogistics(candidate: SlackHomeRankCandidate, nowMs: number): boolean {
const sentAtMs = slackTsToMs(candidate.ts);
if (sentAtMs === null) return false;
if (nowMs - sentAtMs < EXPIRED_ROUTINE_AGE_MS) return false;
const text = candidate.text.replace(/\s+/g, ' ').trim();
if (!ROUTINE_EVENT_RE.test(text)) return false;
if (DURABLE_SIGNAL_RE.test(text)) return false;
return ROUTINE_LOGISTICS_RE.test(text);
}
export function filterSlackHomeCandidatesForRelevance(
candidates: SlackHomeRankCandidate[],
nowMs = Date.now(),
): SlackHomeRankCandidate[] {
return candidates.filter(candidate => !isExpiredRoutineLogistics(candidate, nowMs));
}
function truncate(value: string, max: number): string {
return value.length <= max ? value : `${value.slice(0, max)}...`;
}
@ -60,8 +90,9 @@ Deprioritize:
- greetings, thanks, jokes, reactions, short acknowledgements, bot noise
- vague chatter without clear project/action relevance
- near-duplicates of the same point
- routine logistics whose value expires quickly, such as standup scheduling, standup attendance, sick notes, lunch/commute coordination, and "can we move this?" chatter once the event is likely past
Return only ids from the candidate list. Prefer relevance over recency, but use recency as a tiebreaker.
Return only ids from the candidate list. You may return fewer than ${limit} ids if fewer messages are useful. Prefer relevance over recency, but use recency as a tiebreaker.
# Candidates
@ -72,8 +103,10 @@ export async function rankSlackHomeMessages(
candidates: SlackHomeRankCandidate[],
limit: number,
): Promise<string[]> {
if (candidates.length <= limit) {
return timeRank(candidates, limit);
const relevantCandidates = filterSlackHomeCandidatesForRelevance(candidates);
if (relevantCandidates.length <= limit) {
return timeRank(relevantCandidates, limit);
}
try {
@ -85,7 +118,7 @@ export async function rankSlackHomeMessages(
const result = await withUseCase({ useCase: 'knowledge_sync', subUseCase: 'slack_home_rank' }, () => generateObject({
model,
system: 'You rank Slack messages for a personal productivity Home screen. Be selective and return valid ids only.',
prompt: buildPrompt(candidates, limit),
prompt: buildPrompt(relevantCandidates, limit),
schema: RankedSlackMessagesSchema,
}));
@ -97,7 +130,7 @@ export async function rankSlackHomeMessages(
usage: result.usage,
});
const validIds = new Set(candidates.map(candidate => candidate.id));
const validIds = new Set(relevantCandidates.map(candidate => candidate.id));
const ranked = result.object.rankedIds.filter(id => validIds.has(id));
const seen = new Set<string>();
const deduped = ranked.filter(id => {
@ -106,22 +139,9 @@ export async function rankSlackHomeMessages(
return true;
});
if (deduped.length === 0) {
return timeRank(candidates, limit);
}
const fallback = timeRank(candidates, limit);
for (const id of fallback) {
if (deduped.length >= limit) break;
if (!seen.has(id)) {
deduped.push(id);
seen.add(id);
}
}
return deduped.slice(0, limit);
} catch (error) {
console.warn('[SlackHomeRank] LLM ranking failed, falling back to recency:', error);
return timeRank(candidates, limit);
return timeRank(relevantCandidates, limit);
}
}