fix(imessage): unify timeout configuration with configurable probeTimeoutMs

- Add probeTimeoutMs config option to channels.imessage
- Export DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS constant (10s) from probe.ts
- Propagate timeout config through all iMessage probe/RPC operations
- Fix hardcoded 2000ms timeouts that were too short for SSH connections

Closes: timeout issues when using SSH wrapper scripts (imsg-ssh)
main
Yudong Han 2026-02-04 07:58:46 +00:00 committed by Peter Steinberger
parent 78fd194722
commit 78f8a29071
4 changed files with 20 additions and 7 deletions

View File

@ -52,6 +52,8 @@ export type IMessageAccountConfig = {
includeAttachments?: boolean; includeAttachments?: boolean;
/** Max outbound media size in MB. */ /** Max outbound media size in MB. */
mediaMaxMb?: number; mediaMaxMb?: number;
/** Timeout for probe/RPC operations in milliseconds (default: 10000). */
probeTimeoutMs?: number;
/** Outbound text chunk size (chars). Default: 4000. */ /** Outbound text chunk size (chars). Default: 4000. */
textChunkLimit?: number; textChunkLimit?: number;
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */ /** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */

View File

@ -149,6 +149,7 @@ export class IMessageRpcClient {
params: params ?? {}, params: params ?? {},
}; };
const line = `${JSON.stringify(payload)}\n`; const line = `${JSON.stringify(payload)}\n`;
// Default timeout matches DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS from probe.ts
const timeoutMs = opts?.timeoutMs ?? 10_000; const timeoutMs = opts?.timeoutMs ?? 10_000;
const response = new Promise<T>((resolve, reject) => { const response = new Promise<T>((resolve, reject) => {

View File

@ -45,7 +45,7 @@ import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { truncateUtf16Safe } from "../../utils.js"; import { truncateUtf16Safe } from "../../utils.js";
import { resolveIMessageAccount } from "../accounts.js"; import { resolveIMessageAccount } from "../accounts.js";
import { createIMessageRpcClient } from "../client.js"; import { createIMessageRpcClient } from "../client.js";
import { probeIMessage } from "../probe.js"; import { DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS, probeIMessage } from "../probe.js";
import { sendMessageIMessage } from "../send.js"; import { sendMessageIMessage } from "../send.js";
import { import {
formatIMessageChatTarget, formatIMessageChatTarget,
@ -139,6 +139,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024; const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024;
const cliPath = opts.cliPath ?? imessageCfg.cliPath ?? "imsg"; const cliPath = opts.cliPath ?? imessageCfg.cliPath ?? "imsg";
const dbPath = opts.dbPath ?? imessageCfg.dbPath; const dbPath = opts.dbPath ?? imessageCfg.dbPath;
const probeTimeoutMs = imessageCfg.probeTimeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS;
// Resolve remoteHost: explicit config, or auto-detect from SSH wrapper script // Resolve remoteHost: explicit config, or auto-detect from SSH wrapper script
let remoteHost = imessageCfg.remoteHost; let remoteHost = imessageCfg.remoteHost;
@ -618,7 +619,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
abortSignal: opts.abortSignal, abortSignal: opts.abortSignal,
runtime, runtime,
check: async () => { check: async () => {
const probe = await probeIMessage(2000, { cliPath, dbPath, runtime }); const probe = await probeIMessage(probeTimeoutMs, { cliPath, dbPath, runtime });
if (probe.ok) { if (probe.ok) {
return { ok: true }; return { ok: true };
} }

View File

@ -4,6 +4,9 @@ import { loadConfig } from "../config/config.js";
import { runCommandWithTimeout } from "../process/exec.js"; import { runCommandWithTimeout } from "../process/exec.js";
import { createIMessageRpcClient } from "./client.js"; import { createIMessageRpcClient } from "./client.js";
/** Default timeout for iMessage probe operations (10 seconds). */
export const DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS = 10_000;
export type IMessageProbe = { export type IMessageProbe = {
ok: boolean; ok: boolean;
error?: string | null; error?: string | null;
@ -24,13 +27,13 @@ type RpcSupportResult = {
const rpcSupportCache = new Map<string, RpcSupportResult>(); const rpcSupportCache = new Map<string, RpcSupportResult>();
async function probeRpcSupport(cliPath: string): Promise<RpcSupportResult> { async function probeRpcSupport(cliPath: string, timeoutMs: number): Promise<RpcSupportResult> {
const cached = rpcSupportCache.get(cliPath); const cached = rpcSupportCache.get(cliPath);
if (cached) { if (cached) {
return cached; return cached;
} }
try { try {
const result = await runCommandWithTimeout([cliPath, "rpc", "--help"], { timeoutMs: 2000 }); const result = await runCommandWithTimeout([cliPath, "rpc", "--help"], { timeoutMs });
const combined = `${result.stdout}\n${result.stderr}`.trim(); const combined = `${result.stdout}\n${result.stderr}`.trim();
const normalized = combined.toLowerCase(); const normalized = combined.toLowerCase();
if (normalized.includes("unknown command") && normalized.includes("rpc")) { if (normalized.includes("unknown command") && normalized.includes("rpc")) {
@ -57,18 +60,24 @@ async function probeRpcSupport(cliPath: string): Promise<RpcSupportResult> {
} }
export async function probeIMessage( export async function probeIMessage(
timeoutMs = 2000, timeoutMs = DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS,
opts: IMessageProbeOptions = {}, opts: IMessageProbeOptions = {},
): Promise<IMessageProbe> { ): Promise<IMessageProbe> {
const cfg = opts.cliPath || opts.dbPath ? undefined : loadConfig(); const cfg = opts.cliPath || opts.dbPath ? undefined : loadConfig();
const cliPath = opts.cliPath?.trim() || cfg?.channels?.imessage?.cliPath?.trim() || "imsg"; const cliPath = opts.cliPath?.trim() || cfg?.channels?.imessage?.cliPath?.trim() || "imsg";
const dbPath = opts.dbPath?.trim() || cfg?.channels?.imessage?.dbPath?.trim(); const dbPath = opts.dbPath?.trim() || cfg?.channels?.imessage?.dbPath?.trim();
// Read probeTimeoutMs from config if not explicitly provided
const effectiveTimeout =
timeoutMs !== DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS
? timeoutMs
: cfg?.channels?.imessage?.probeTimeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS;
const detected = await detectBinary(cliPath); const detected = await detectBinary(cliPath);
if (!detected) { if (!detected) {
return { ok: false, error: `imsg not found (${cliPath})` }; return { ok: false, error: `imsg not found (${cliPath})` };
} }
const rpcSupport = await probeRpcSupport(cliPath); const rpcSupport = await probeRpcSupport(cliPath, effectiveTimeout);
if (!rpcSupport.supported) { if (!rpcSupport.supported) {
return { return {
ok: false, ok: false,
@ -83,7 +92,7 @@ export async function probeIMessage(
runtime: opts.runtime, runtime: opts.runtime,
}); });
try { try {
await client.request("chats.list", { limit: 1 }, { timeoutMs }); await client.request("chats.list", { limit: 1 }, { timeoutMs: effectiveTimeout });
return { ok: true }; return { ok: true };
} catch (err) { } catch (err) {
return { ok: false, error: String(err) }; return { ok: false, error: String(err) };