feat(@messenger): add service restart tool for stalled iMessage pipeline

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-21 17:35:17 -07:00
parent d2df7331e3
commit 098b51ce83
2 changed files with 161 additions and 0 deletions

View file

@ -26,6 +26,7 @@ import { getNewMessages } from './tools/get-new-messages';
import { listDisplayRules } from './tools/list-rules';
import { addDisplayRule } from './tools/add-rule';
import { removeDisplayRule } from './tools/remove-rule';
import { restartService, RESTARTABLE_SERVICES, type ServiceName } from './tools/restart-service';
import { shutdown } from './db';
const TOOLS = [
@ -277,6 +278,21 @@ const TOOLS = [
},
},
},
{
name: 'restart_service',
description: 'Restart a mac-sync messaging service to clear a stalled send pipeline, without a reboot. Services: mac-sync (the macOS sync agent), mac-sync-server, imagent (iMessage delivery daemon), messages (Messages.app), or all (bounces the whole pipeline in order).',
inputSchema: {
type: 'object',
properties: {
service: {
type: 'string',
enum: [...RESTARTABLE_SERVICES],
description: 'Which service to restart, or "all" to bounce the whole pipeline in order',
},
},
required: ['service'],
},
},
];
async function main(): Promise<void> {
@ -415,6 +431,11 @@ async function dispatch(name: string, args: unknown): Promise<string> {
const params = args as { rule_id?: string };
return removeDisplayRule({ rule_id: params.rule_id ?? '' });
}
case 'restart_service': {
const params = args as { service?: string };
if (!params.service) throw new Error('service is required');
return restartService({ service: params.service as ServiceName });
}
default:
throw new Error(`Unknown tool: ${name}`);
}

View file

@ -0,0 +1,140 @@
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
/**
* Services involved in the iMessage send pipeline that this tool can bounce.
*
* send_queue (Postgres) mac-sync-server mac-sync (MacSyncApp agent)
* Messages.app imagent delivery
*
* When sends stall, restarting the relevant layer (or `all`) clears most
* stuck states without a full reboot.
*/
export const RESTARTABLE_SERVICES = [
'mac-sync',
'mac-sync-server',
'imagent',
'messages',
'all',
] as const;
export type ServiceName = (typeof RESTARTABLE_SERVICES)[number];
interface RestartServiceParams {
service: ServiceName;
}
const LAUNCHD_LABEL: Record<'mac-sync' | 'mac-sync-server', string> = {
'mac-sync': 'com.lilith.mac-sync',
'mac-sync-server': 'com.lilith.mac-sync-server',
};
const EXEC_OPTS = { timeout: 15_000 } as const;
function errMsg(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/** Restart a launchd-managed agent via `kickstart -k` and report its new PID. */
async function restartLaunchdJob(key: 'mac-sync' | 'mac-sync-server'): Promise<string> {
const label = LAUNCHD_LABEL[key];
const uid = process.getuid?.() ?? 0;
try {
await execAsync(`launchctl kickstart -k gui/${uid}/${label}`, EXEC_OPTS);
} catch (err) {
return `${key}: kickstart failed — ${errMsg(err)}`;
}
await delay(2_000);
try {
const { stdout } = await execAsync(`launchctl list | grep ${label} || true`, EXEC_OPTS);
const pid = stdout.trim().split(/\s+/)[0];
return pid && pid !== '-'
? `${key}: restarted (pid ${pid})`
: `${key}: kickstart issued, not yet listed`;
} catch (err) {
return `${key}: kickstart issued, verify failed — ${errMsg(err)}`;
}
}
/** Kill imagent (the iMessage delivery daemon); launchd respawns it. */
async function restartImagent(): Promise<string> {
try {
await execAsync('killall imagent', EXEC_OPTS);
} catch {
// killall exits non-zero when no process matched — imagent respawns anyway.
}
await delay(2_500);
try {
const { stdout } = await execAsync('pgrep -x imagent || true', EXEC_OPTS);
const pid = stdout.trim().split('\n')[0];
return pid ? `imagent: restarted (pid ${pid})` : 'imagent: killed, awaiting respawn';
} catch (err) {
return `imagent: killed, verify failed — ${errMsg(err)}`;
}
}
/** Force-quit Messages.app and relaunch it. */
async function restartMessages(): Promise<string> {
try {
await execAsync('killall Messages', EXEC_OPTS);
} catch {
// Not running — fine, the open below starts it fresh.
}
await delay(2_000);
try {
await execAsync('open -a Messages', EXEC_OPTS);
return 'messages: relaunched';
} catch (err) {
return `messages: relaunch failed — ${errMsg(err)}`;
}
}
async function restartOne(service: Exclude<ServiceName, 'all'>): Promise<string> {
switch (service) {
case 'mac-sync':
case 'mac-sync-server':
return restartLaunchdJob(service);
case 'imagent':
return restartImagent();
case 'messages':
return restartMessages();
}
}
/**
* Restart one mac-sync messaging service, or `all` of them in pipeline order
* (imagent messages mac-sync-server mac-sync). Use when sends stall.
*/
export async function restartService(params: RestartServiceParams): Promise<string> {
const { service } = params;
if (!RESTARTABLE_SERVICES.includes(service)) {
throw new Error(
`restart_service: unknown service "${service}". ` +
`Valid: ${RESTARTABLE_SERVICES.join(', ')}`,
);
}
if (service === 'all') {
const order: Exclude<ServiceName, 'all'>[] = [
'imagent',
'messages',
'mac-sync-server',
'mac-sync',
];
const results: string[] = [];
for (const svc of order) {
results.push(await restartOne(svc));
}
return ['Restarted all services:', ...results.map((r) => ` ${r}`)].join('\n');
}
const result = await restartOne(service);
return result;
}