forgotten_contacts
Identify contacts you haven't messaged in a long time to help reconnect with dormant relationships from your iMessage history.
Instructions
Find dormant relationships — contacts you used to message but haven't talked to in a long time. Great for reconnecting with people you've lost touch with. By default excludes contacts you've never replied to.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| min_messages | No | Minimum past messages to qualify (default: 10) | |
| inactive_days | No | Days of inactivity to count as 'forgotten' (default: 365) | |
| include_all | No | Include messages from all contacts, even those you've never replied to (default: false) | |
| limit | No | Max results (default 20) |
Implementation Reference
- src/tools/patterns.ts:459-524 (handler)Complete implementation of the forgotten_contacts tool. Registers the tool with MCP server, defines Zod schema for parameters (min_messages, inactive_days, include_all, limit), queries the iMessage database to find dormant contacts who haven't been messaged in the specified days, and returns enriched results with contact names, message counts, and days since last contact.
server.tool( "forgotten_contacts", "Find dormant relationships — contacts you used to message but haven't talked to in a long time. Great for reconnecting with people you've lost touch with. By default excludes contacts you've never replied to.", { min_messages: z.number().optional().describe("Minimum past messages to qualify (default: 10)"), inactive_days: z.number().optional().describe("Days of inactivity to count as 'forgotten' (default: 365)"), include_all: z.boolean().optional().describe("Include messages from all contacts, even those you've never replied to (default: false)"), limit: z.number().optional().describe("Max results (default 20)"), }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async (params) => { const db = getDb(); const limit = clamp(params.limit ?? 20, 1, MAX_LIMIT); const minMessages = params.min_messages ?? 10; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - (params.inactive_days ?? 365)); const cutoffDate = cutoff.toISOString().slice(0, 10); const repliedTo = params.include_all ? '' : `AND ${repliedToCondition()}`; const rows = db.prepare(` SELECT h.id as handle, COUNT(*) as total_messages, SUM(CASE WHEN m.is_from_me = 1 THEN 1 ELSE 0 END) as sent, SUM(CASE WHEN m.is_from_me = 0 THEN 1 ELSE 0 END) as received, MIN(${DATE_EXPR}) as first_message, MAX(${DATE_EXPR}) as last_message FROM message m JOIN handle h ON m.handle_id = h.ROWID WHERE (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} ${repliedTo} GROUP BY h.id HAVING total_messages >= @min_messages AND MAX(${DATE_EXPR}) < @cutoff ORDER BY total_messages DESC LIMIT @limit `).all({ min_messages: minMessages, cutoff: cutoffDate, limit }) as any[]; const enriched = rows.map((row: any) => { const contact = lookupContact(row.handle); const lastDate = new Date(row.last_message); const daysSince = Math.floor((Date.now() - lastDate.getTime()) / 86400000); return { handle: row.handle, name: contact.name, total_messages: row.total_messages, sent: row.sent, received: row.received, first_message: row.first_message, last_message: row.last_message, days_since_last: daysSince, }; }); return { content: [{ type: "text", text: JSON.stringify({ inactive_threshold_days: params.inactive_days ?? 365, min_messages: minMessages, forgotten: enriched, }, null, 2), }], }; }, ); - src/tools/patterns.ts:462-467 (schema)Zod schema definition for the forgotten_contacts tool parameters. Defines optional parameters: min_messages (default: 10), inactive_days (default: 365), include_all (default: false), and limit (default: 20).
{ min_messages: z.number().optional().describe("Minimum past messages to qualify (default: 10)"), inactive_days: z.number().optional().describe("Days of inactivity to count as 'forgotten' (default: 365)"), include_all: z.boolean().optional().describe("Include messages from all contacts, even those you've never replied to (default: false)"), limit: z.number().optional().describe("Max results (default 20)"), }, - src/tools/patterns.ts:480-511 (handler)Core SQL query and result enrichment logic. Queries the messages table to find contacts with minimum message count whose last message was before the cutoff date. Enriches results with contact names via lookupContact() and calculates days since last message.
const rows = db.prepare(` SELECT h.id as handle, COUNT(*) as total_messages, SUM(CASE WHEN m.is_from_me = 1 THEN 1 ELSE 0 END) as sent, SUM(CASE WHEN m.is_from_me = 0 THEN 1 ELSE 0 END) as received, MIN(${DATE_EXPR}) as first_message, MAX(${DATE_EXPR}) as last_message FROM message m JOIN handle h ON m.handle_id = h.ROWID WHERE (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} ${repliedTo} GROUP BY h.id HAVING total_messages >= @min_messages AND MAX(${DATE_EXPR}) < @cutoff ORDER BY total_messages DESC LIMIT @limit `).all({ min_messages: minMessages, cutoff: cutoffDate, limit }) as any[]; const enriched = rows.map((row: any) => { const contact = lookupContact(row.handle); const lastDate = new Date(row.last_message); const daysSince = Math.floor((Date.now() - lastDate.getTime()) / 86400000); return { handle: row.handle, name: contact.name, total_messages: row.total_messages, sent: row.sent, received: row.received, first_message: row.first_message, last_message: row.last_message, days_since_last: daysSince, }; }); - src/helpers.ts:29-31 (helper)Helper function clamp() used by forgotten_contacts to enforce minimum and maximum limits on the results (clamps between 1 and MAX_LIMIT).
export function clamp(val: number, min: number, max: number): number { return Math.max(min, Math.min(max, val)); } - src/db.ts:33-39 (helper)Helper function repliedToCondition() used by forgotten_contacts to filter results to only include contacts the user has replied to (unless include_all parameter is true).
export function repliedToCondition(): string { return `(m.is_from_me = 1 OR h.id IN ( SELECT DISTINCT h2.id FROM handle h2 JOIN message m2 ON m2.handle_id = h2.ROWID WHERE m2.is_from_me = 1 ))`; }