fix: imessage dm replies and error details (#935)

This commit is contained in:
Peter Steinberger
2026-01-15 07:58:44 +00:00
parent 9c04a79c0a
commit a5a9788b20
7 changed files with 88 additions and 6 deletions

View File

@@ -57,6 +57,7 @@
#### Messaging / Channels #### Messaging / Channels
- Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose. - Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose.
- iMessage: prefer handle routing for direct-message replies; include imsg RPC error details. (#935)
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr. - Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.
- Slack: drop Socket Mode events with mismatched `api_app_id`/`team_id`. (#889) — thanks @roshanasingh4. - Slack: drop Socket Mode events with mismatched `api_app_id`/`team_id`. (#889) — thanks @roshanasingh4.
- Discord: isolate autoThread thread context. (#856) — thanks @davidguttman. - Discord: isolate autoThread thread context. (#856) — thanks @davidguttman.

View File

@@ -53,4 +53,38 @@ describe("buildThreadingToolContext", () => {
expect(result.currentChannelId).toBe("chat:99"); expect(result.currentChannelId).toBe("chat:99");
}); });
it("uses the sender handle for iMessage direct chats", () => {
const sessionCtx = {
Provider: "imessage",
ChatType: "direct",
From: "imessage:+15550001",
To: "chat_id:12",
} as TemplateContext;
const result = buildThreadingToolContext({
sessionCtx,
config: cfg,
hasRepliedRef: undefined,
});
expect(result.currentChannelId).toBe("imessage:+15550001");
});
it("uses chat_id for iMessage groups", () => {
const sessionCtx = {
Provider: "imessage",
ChatType: "group",
From: "group:7",
To: "chat_id:7",
} as TemplateContext;
const result = buildThreadingToolContext({
sessionCtx,
config: cfg,
hasRepliedRef: undefined,
});
expect(result.currentChannelId).toBe("chat_id:7");
});
}); });

View File

@@ -26,7 +26,12 @@ export function buildThreadingToolContext(params: {
const dock = getChannelDock(provider); const dock = getChannelDock(provider);
if (!dock?.threading?.buildToolContext) return {}; if (!dock?.threading?.buildToolContext) return {};
// WhatsApp context isolation keys off conversation id, not the bot's own number. // WhatsApp context isolation keys off conversation id, not the bot's own number.
const threadingTo = provider === "whatsapp" ? (sessionCtx.From ?? sessionCtx.To) : sessionCtx.To; const threadingTo =
provider === "whatsapp"
? (sessionCtx.From ?? sessionCtx.To)
: provider === "imessage" && sessionCtx.ChatType === "direct"
? (sessionCtx.From ?? sessionCtx.To)
: sessionCtx.To;
return ( return (
dock.threading.buildToolContext({ dock.threading.buildToolContext({
cfg: config, cfg: config,

View File

@@ -180,7 +180,17 @@ export class IMessageRpcClient {
this.pending.delete(key); this.pending.delete(key);
if (parsed.error) { if (parsed.error) {
const msg = parsed.error.message ?? "imsg rpc error"; const baseMessage = parsed.error.message ?? "imsg rpc error";
const details = parsed.error.data;
const code = parsed.error.code;
const suffixes = [] as string[];
if (typeof code === "number") suffixes.push(`code=${code}`);
if (details !== undefined) {
const detailText =
typeof details === "string" ? details : JSON.stringify(details, null, 2);
if (detailText) suffixes.push(detailText);
}
const msg = suffixes.length > 0 ? `${baseMessage}: ${suffixes.join(" ")}` : baseMessage;
pending.reject(new Error(msg)); pending.reject(new Error(msg));
return; return;
} }

View File

@@ -92,7 +92,7 @@ beforeEach(() => {
}); });
describe("monitorIMessageProvider", () => { describe("monitorIMessageProvider", () => {
it("updates last route with chat_id for direct messages", async () => { it("updates last route with sender handle for direct messages", async () => {
replyMock.mockResolvedValueOnce({ text: "ok" }); replyMock.mockResolvedValueOnce({ text: "ok" });
const run = monitorIMessageProvider(); const run = monitorIMessageProvider();
await waitForSubscribe(); await waitForSubscribe();
@@ -118,7 +118,7 @@ describe("monitorIMessageProvider", () => {
expect(updateLastRouteMock).toHaveBeenCalledWith( expect(updateLastRouteMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
channel: "imessage", channel: "imessage",
to: "chat_id:7", to: "+15550004444",
}), }),
); );
}); });

View File

@@ -296,7 +296,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
}); });
} }
const imessageTo = chatTarget || `imessage:${sender}`; const imessageTo = (isGroup ? chatTarget : undefined) || `imessage:${sender}`;
const ctxPayload = { const ctxPayload = {
Body: combinedBody, Body: combinedBody,
RawBody: bodyText, RawBody: bodyText,
@@ -329,7 +329,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
const storePath = resolveStorePath(sessionCfg?.store, { const storePath = resolveStorePath(sessionCfg?.store, {
agentId: route.agentId, agentId: route.agentId,
}); });
const to = chatTarget || sender; const to = (isGroup ? chatTarget : undefined) || sender;
if (to) { if (to) {
await updateLastRoute({ await updateLastRoute({
storePath, storePath,

View File

@@ -100,4 +100,36 @@ describe("runMessageAction context isolation", () => {
}), }),
).rejects.toThrow(/Cross-context messaging denied/); ).rejects.toThrow(/Cross-context messaging denied/);
}); });
it("allows iMessage send when target matches current handle", async () => {
const result = await runMessageAction({
cfg: whatsappConfig,
action: "send",
params: {
channel: "imessage",
to: "imessage:+15551234567",
message: "hi",
},
toolContext: { currentChannelId: "imessage:+15551234567" },
dryRun: true,
});
expect(result.kind).toBe("send");
});
it("blocks iMessage send when target differs from current handle", async () => {
await expect(
runMessageAction({
cfg: whatsappConfig,
action: "send",
params: {
channel: "imessage",
to: "imessage:+15551230000",
message: "hi",
},
toolContext: { currentChannelId: "imessage:+15551234567" },
dryRun: true,
}),
).rejects.toThrow(/Cross-context messaging denied/);
});
}); });