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)
This commit is contained in:
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;
/** Max outbound media size in MB. */
mediaMaxMb?: number;
/** Timeout for probe/RPC operations in milliseconds (default: 10000). */
probeTimeoutMs?: number;
/** Outbound text chunk size (chars). Default: 4000. */
textChunkLimit?: number;
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */

View File

@@ -149,6 +149,7 @@ export class IMessageRpcClient {
params: params ?? {},
};
const line = `${JSON.stringify(payload)}\n`;
// Default timeout matches DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS from probe.ts
const timeoutMs = opts?.timeoutMs ?? 10_000;
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 { resolveIMessageAccount } from "../accounts.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 {
formatIMessageChatTarget,
@@ -139,6 +139,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024;
const cliPath = opts.cliPath ?? imessageCfg.cliPath ?? "imsg";
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
let remoteHost = imessageCfg.remoteHost;
@@ -618,7 +619,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
abortSignal: opts.abortSignal,
runtime,
check: async () => {
const probe = await probeIMessage(2000, { cliPath, dbPath, runtime });
const probe = await probeIMessage(probeTimeoutMs, { cliPath, dbPath, runtime });
if (probe.ok) {
return { ok: true };
}

View File

@@ -4,6 +4,9 @@ import { loadConfig } from "../config/config.js";
import { runCommandWithTimeout } from "../process/exec.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 = {
ok: boolean;
error?: string | null;
@@ -24,13 +27,13 @@ type 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);
if (cached) {
return cached;
}
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 normalized = combined.toLowerCase();
if (normalized.includes("unknown command") && normalized.includes("rpc")) {
@@ -57,18 +60,24 @@ async function probeRpcSupport(cliPath: string): Promise<RpcSupportResult> {
}
export async function probeIMessage(
timeoutMs = 2000,
timeoutMs = DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS,
opts: IMessageProbeOptions = {},
): Promise<IMessageProbe> {
const cfg = opts.cliPath || opts.dbPath ? undefined : loadConfig();
const cliPath = opts.cliPath?.trim() || cfg?.channels?.imessage?.cliPath?.trim() || "imsg";
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);
if (!detected) {
return { ok: false, error: `imsg not found (${cliPath})` };
}
const rpcSupport = await probeRpcSupport(cliPath);
const rpcSupport = await probeRpcSupport(cliPath, effectiveTimeout);
if (!rpcSupport.supported) {
return {
ok: false,
@@ -83,7 +92,7 @@ export async function probeIMessage(
runtime: opts.runtime,
});
try {
await client.request("chats.list", { limit: 1 }, { timeoutMs });
await client.request("chats.list", { limit: 1 }, { timeoutMs: effectiveTimeout });
return { ok: true };
} catch (err) {
return { ok: false, error: String(err) };