Skip to content

Commit 78595d9

Browse files
author
OpenClaw Codex
committed
fix(imessage): honor block streaming config
Route iMessage inbound reply dispatch through the shared block-streaming resolver so the channel respects schema-valid channels.imessage.blockStreaming values instead of bypassing them on the monitor path. Also keep the iMessage streaming schema/tests aligned with the shared channel delivery streaming shape.
1 parent 7a602c7 commit 78595d9

10 files changed

Lines changed: 444 additions & 32 deletions

extensions/imessage/src/accounts.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
resolveMergedAccountConfig,
77
type OpenClawConfig,
88
} from "openclaw/plugin-sdk/account-resolution";
9+
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
910
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
1011
import type { IMessageAccountConfig } from "./account-types.js";
1112

@@ -25,14 +26,95 @@ const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("im
2526
export const listIMessageAccountIds = listAccountIds;
2627
export const resolveDefaultIMessageAccountId = resolveDefaultAccountId;
2728

29+
function resolveIMessageAccountConfig(
30+
cfg: OpenClawConfig,
31+
accountId: string,
32+
): IMessageAccountConfig | undefined {
33+
return resolveAccountEntry(cfg.channels?.imessage?.accounts, accountId);
34+
}
35+
36+
type IMessageStreamingConfig = NonNullable<IMessageAccountConfig["streaming"]>;
37+
38+
function asStreamingConfigObject(value: unknown): IMessageStreamingConfig | undefined {
39+
return value && typeof value === "object" && !Array.isArray(value)
40+
? (value as IMessageStreamingConfig)
41+
: undefined;
42+
}
43+
44+
function asOwnBooleanProperty(value: unknown, key: string): boolean | undefined {
45+
if (!value || typeof value !== "object" || Array.isArray(value)) {
46+
return undefined;
47+
}
48+
const record = value as Record<string, unknown>;
49+
return Object.hasOwn(record, key) && typeof record[key] === "boolean" ? record[key] : undefined;
50+
}
51+
52+
function mergeIMessageStreamingConfig(
53+
base: unknown,
54+
account: unknown,
55+
accountFlatBlockStreaming: unknown,
56+
): IMessageStreamingConfig | undefined {
57+
const baseConfig = asStreamingConfigObject(base);
58+
const accountConfig = asStreamingConfigObject(account);
59+
const accountBlockEnabled = asOwnBooleanProperty(accountConfig?.block, "enabled");
60+
const flatAccountBlockEnabled =
61+
accountBlockEnabled === undefined && typeof accountFlatBlockStreaming === "boolean"
62+
? accountFlatBlockStreaming
63+
: undefined;
64+
const applyFlatAccountBlockEnabled = (
65+
config: IMessageStreamingConfig | undefined,
66+
): IMessageStreamingConfig | undefined => {
67+
if (flatAccountBlockEnabled === undefined || config === undefined) {
68+
return config;
69+
}
70+
return {
71+
...config,
72+
block: {
73+
...config.block,
74+
enabled: flatAccountBlockEnabled,
75+
},
76+
};
77+
};
78+
if (!baseConfig || !accountConfig) {
79+
return applyFlatAccountBlockEnabled(accountConfig ?? baseConfig);
80+
}
81+
return applyFlatAccountBlockEnabled({
82+
...baseConfig,
83+
...accountConfig,
84+
...(baseConfig.block || accountConfig.block
85+
? {
86+
block: {
87+
...baseConfig.block,
88+
...accountConfig.block,
89+
...(baseConfig.block?.coalesce || accountConfig.block?.coalesce
90+
? {
91+
coalesce: {
92+
...baseConfig.block?.coalesce,
93+
...accountConfig.block?.coalesce,
94+
},
95+
}
96+
: {}),
97+
},
98+
}
99+
: {}),
100+
});
101+
}
102+
28103
function mergeIMessageAccountConfig(cfg: OpenClawConfig, accountId: string): IMessageAccountConfig {
29-
return resolveMergedAccountConfig<IMessageAccountConfig>({
104+
const accountConfig = resolveIMessageAccountConfig(cfg, accountId);
105+
const merged = resolveMergedAccountConfig<IMessageAccountConfig>({
30106
channelConfig: cfg.channels?.imessage as IMessageAccountConfig | undefined,
31107
accounts: cfg.channels?.imessage?.accounts as
32108
| Record<string, Partial<IMessageAccountConfig>>
33109
| undefined,
34110
accountId,
35111
});
112+
const streaming = mergeIMessageStreamingConfig(
113+
(cfg.channels?.imessage as Record<string, unknown> | undefined)?.streaming,
114+
(accountConfig as Record<string, unknown> | undefined)?.streaming,
115+
(accountConfig as Record<string, unknown> | undefined)?.blockStreaming,
116+
);
117+
return streaming !== undefined ? ({ ...merged, streaming } as IMessageAccountConfig) : merged;
36118
}
37119

38120
export function resolveIMessageAccount(params: {

extensions/imessage/src/config-schema.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,31 @@ describe("imessage config schema", () => {
7272
}
7373
});
7474

75+
it("accepts nested delivery streaming config", () => {
76+
const res = IMessageConfigSchema.safeParse({
77+
enabled: true,
78+
streaming: {
79+
chunkMode: "newline",
80+
block: {
81+
enabled: true,
82+
coalesce: { minChars: 200, idleMs: 50 },
83+
},
84+
},
85+
accounts: {
86+
personal: {
87+
streaming: { chunkMode: "length", block: { enabled: false } },
88+
},
89+
},
90+
});
91+
92+
expect(res.success).toBe(true);
93+
if (res.success) {
94+
expect(res.data.streaming?.chunkMode).toBe("newline");
95+
expect(res.data.streaming?.block?.enabled).toBe(true);
96+
expect(res.data.accounts?.personal?.streaming?.block?.enabled).toBe(false);
97+
}
98+
});
99+
75100
it("accepts reaction notification mode overrides", () => {
76101
const res = IMessageConfigSchema.safeParse({
77102
reactionNotifications: "all",

0 commit comments

Comments
 (0)