Skip to content

Commit 1d34564

Browse files
committed
fix(plugins): expose hook timeout overrides
1 parent c5488ea commit 1d34564

17 files changed

Lines changed: 243 additions & 5 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
3030
### Fixes
3131

3232
- Gateway/usage: serve `usage.cost` and `sessions.usage` from a durable transcript aggregate cache with lock-safe background refreshes and localized stale-cache status, so large usage views avoid repeated full scans. (#76650) Thanks @Marvinthebored.
33+
- Plugins/hooks: let `plugins.entries.<id>.hooks.timeoutMs` and `plugins.entries.<id>.hooks.timeouts` bound plugin typed hooks from operator config, so slow hooks can be tuned without patching installed plugin code. Fixes #76778. Thanks @vincentkoc.
3334
- Telegram: add `channels.telegram.mediaGroupFlushMs` at the top level and per account so operators can tune album buffering instead of being stuck with the hard-coded 500ms media-group flush window. Fixes #76149. Thanks @vincentkoc.
3435
- Config/messages: coerce boolean `messages.visibleReplies` and `messages.groupChat.visibleReplies` values to the documented enum modes so an intuitive toggle no longer invalidates config and drops channel startup. Fixes #75390. Thanks @scottgl9.
3536
- Feishu: accept and honor `channels.feishu.blockStreaming` at the top level and per account, while keeping the legacy default off so Feishu cards no longer reject documented config or silently drop block replies. Fixes #75555. Thanks @vincentkoc.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
df881d10bfb3d1ba0439e5984117dde70b5f7e856696f25c7f4b5c978a38f841 config-baseline.json
1+
3a6c1626e7f5f6c7c8658516072e9ab327b668f6b25ecd3ab1e12cbcb6dc1f88 config-baseline.json
22
f945a060012b3e7c675fb3ea0c5f18996cdcc06c9ec6cead389e04791a529ce9 config-baseline.core.json
33
09a952cf734a5b4a30f760e570c0f106d54aa8e74bf439dd4d07013f9f7607e4 config-baseline.channel.json
4-
245aa98aabc6c2e3c57a69e639c2fb10d84a7e1e1b3bcdadc340fa61ca998287 config-baseline.plugin.json
4+
055fae0d0067a751dc10125af7421da45633f73519c94c982d02b0c4eb2bdf67 config-baseline.plugin.json

docs/plugins/hooks.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,32 @@ keep registration order.
6161
timeout. Omit it to use the default observation/decision timeout that the
6262
hook runner applies generically.
6363

64+
Operators can also set hook budgets without patching plugin code:
65+
66+
```json
67+
{
68+
"plugins": {
69+
"entries": {
70+
"my-plugin": {
71+
"hooks": {
72+
"timeoutMs": 30000,
73+
"timeouts": {
74+
"before_prompt_build": 90000,
75+
"agent_end": 60000
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
82+
```
83+
84+
`hooks.timeouts.<hookName>` overrides `hooks.timeoutMs`, which overrides the
85+
plugin-authored `api.on(..., { timeoutMs })` value. Each configured value must
86+
be a positive integer no greater than 600000 milliseconds. Prefer per-hook
87+
overrides for known slow hooks so one plugin does not get a longer budget
88+
everywhere.
89+
6490
Each hook receives `event.context.pluginConfig`, the resolved config for the
6591
plugin that registered that handler. Use it for hook decisions that need
6692
current plugin options; OpenClaw injects it per handler without mutating the

src/config/config-misc.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,25 @@ describe("plugins.entries.*.hooks", () => {
374374
expect(result.success).toBe(true);
375375
});
376376

377+
it("accepts bounded typed hook timeout overrides", () => {
378+
const result = OpenClawSchema.safeParse({
379+
plugins: {
380+
entries: {
381+
"memory-recall": {
382+
hooks: {
383+
timeoutMs: 30_000,
384+
timeouts: {
385+
before_prompt_build: 90_000,
386+
agent_end: 60_000,
387+
},
388+
},
389+
},
390+
},
391+
},
392+
});
393+
expect(result.success).toBe(true);
394+
});
395+
377396
it("rejects non-boolean values", () => {
378397
const result = OpenClawSchema.safeParse({
379398
plugins: {
@@ -405,6 +424,24 @@ describe("plugins.entries.*.hooks", () => {
405424
});
406425
expect(result.success).toBe(false);
407426
});
427+
428+
it("rejects invalid typed hook timeout overrides", () => {
429+
for (const hooks of [
430+
{ timeoutMs: 0 },
431+
{ timeoutMs: 600_001 },
432+
{ timeouts: { before_prompt_build: -1 } },
433+
{ timeouts: { before_prompt_build: 1.5 } },
434+
]) {
435+
const result = OpenClawSchema.safeParse({
436+
plugins: {
437+
entries: {
438+
"memory-recall": { hooks },
439+
},
440+
},
441+
});
442+
expect(result.success).toBe(false);
443+
}
444+
});
408445
});
409446

410447
describe("plugins.entries.*.subagent", () => {

src/config/schema.base.generated.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24091,6 +24091,28 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
2409124091
description:
2409224092
"Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_agent_finalize`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
2409324093
},
24094+
timeoutMs: {
24095+
type: "integer",
24096+
exclusiveMinimum: 0,
24097+
maximum: 600000,
24098+
title: "Plugin Hook Timeout (ms)",
24099+
description:
24100+
"Default timeout in milliseconds for this plugin's typed hooks, capped at 600000. Use this to bound slow plugin hooks without changing plugin code; per-hook values in hooks.timeouts take precedence.",
24101+
},
24102+
timeouts: {
24103+
type: "object",
24104+
propertyNames: {
24105+
type: "string",
24106+
},
24107+
additionalProperties: {
24108+
type: "integer",
24109+
exclusiveMinimum: 0,
24110+
maximum: 600000,
24111+
},
24112+
title: "Plugin Hook Timeout Overrides",
24113+
description:
24114+
"Per-hook timeout overrides in milliseconds keyed by typed hook name, capped at 600000. Use narrow overrides for known slow hooks such as before_prompt_build or agent_end instead of raising every hook timeout.",
24115+
},
2409424116
},
2409524117
additionalProperties: false,
2409624118
title: "Plugin Hook Policy",
@@ -28867,6 +28889,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
2886728889
help: "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
2886828890
tags: ["access"],
2886928891
},
28892+
"plugins.entries.*.hooks.timeoutMs": {
28893+
label: "Plugin Hook Timeout (ms)",
28894+
help: "Default timeout in milliseconds for this plugin's typed hooks, capped at 600000. Use this to bound slow plugin hooks without changing plugin code; per-hook values in hooks.timeouts take precedence.",
28895+
tags: ["performance"],
28896+
},
28897+
"plugins.entries.*.hooks.timeouts": {
28898+
label: "Plugin Hook Timeout Overrides",
28899+
help: "Per-hook timeout overrides in milliseconds keyed by typed hook name, capped at 600000. Use narrow overrides for known slow hooks such as before_prompt_build or agent_end instead of raising every hook timeout.",
28900+
tags: ["performance"],
28901+
},
2887028902
"plugins.entries.*.subagent": {
2887128903
label: "Plugin Subagent Policy",
2887228904
help: "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.",

src/config/schema.help.quality.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,8 @@ const TARGET_KEYS = [
369369
"plugins.entries.*.hooks",
370370
"plugins.entries.*.hooks.allowPromptInjection",
371371
"plugins.entries.*.hooks.allowConversationAccess",
372+
"plugins.entries.*.hooks.timeoutMs",
373+
"plugins.entries.*.hooks.timeouts",
372374
"plugins.entries.*.subagent",
373375
"plugins.entries.*.subagent.allowModelOverride",
374376
"plugins.entries.*.subagent.allowedModels",
@@ -800,6 +802,14 @@ describe("config help copy quality", () => {
800802
expect(pluginConversationPolicy.includes("llm_input")).toBe(true);
801803
expect(pluginConversationPolicy.includes("llm_output")).toBe(true);
802804
expect(pluginConversationPolicy.includes("before_agent_finalize")).toBe(true);
805+
806+
const pluginHookTimeout = FIELD_HELP["plugins.entries.*.hooks.timeoutMs"];
807+
expect(pluginHookTimeout.includes("typed hooks")).toBe(true);
808+
expect(pluginHookTimeout.includes("hooks.timeouts")).toBe(true);
809+
810+
const pluginHookTimeouts = FIELD_HELP["plugins.entries.*.hooks.timeouts"];
811+
expect(pluginHookTimeouts.includes("before_prompt_build")).toBe(true);
812+
expect(pluginHookTimeouts.includes("agent_end")).toBe(true);
803813
expect(pluginConversationPolicy.includes("agent_end")).toBe(true);
804814
});
805815

src/config/schema.help.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,10 @@ export const FIELD_HELP: Record<string, string> = {
12201220
"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
12211221
"plugins.entries.*.hooks.allowConversationAccess":
12221222
"Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_agent_finalize`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
1223+
"plugins.entries.*.hooks.timeoutMs":
1224+
"Default timeout in milliseconds for this plugin's typed hooks, capped at 600000. Use this to bound slow plugin hooks without changing plugin code; per-hook values in hooks.timeouts take precedence.",
1225+
"plugins.entries.*.hooks.timeouts":
1226+
"Per-hook timeout overrides in milliseconds keyed by typed hook name, capped at 600000. Use narrow overrides for known slow hooks such as before_prompt_build or agent_end instead of raising every hook timeout.",
12231227
"plugins.entries.*.subagent":
12241228
"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.",
12251229
"plugins.entries.*.subagent.allowModelOverride":

src/config/schema.labels.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,8 @@ export const FIELD_LABELS: Record<string, string> = {
915915
"plugins.entries.*.hooks": "Plugin Hook Policy",
916916
"plugins.entries.*.hooks.allowConversationAccess": "Allow Conversation Access Hooks",
917917
"plugins.entries.*.hooks.allowPromptInjection": "Allow Prompt Injection Hooks",
918+
"plugins.entries.*.hooks.timeoutMs": "Plugin Hook Timeout (ms)",
919+
"plugins.entries.*.hooks.timeouts": "Plugin Hook Timeout Overrides",
918920
"plugins.entries.*.subagent": "Plugin Subagent Policy",
919921
"plugins.entries.*.subagent.allowModelOverride": "Allow Plugin Subagent Model Override",
920922
"plugins.entries.*.subagent.allowedModels": "Plugin Subagent Allowed Models",

src/config/types.plugins.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export type PluginEntryConfig = {
88
* Non-bundled plugins must opt in explicitly; bundled plugins stay allowed unless disabled.
99
*/
1010
allowConversationAccess?: boolean;
11+
/** Default timeout in milliseconds for this plugin's typed hooks. */
12+
timeoutMs?: number;
13+
/** Per typed-hook timeout overrides in milliseconds. */
14+
timeouts?: Record<string, number>;
1115
};
1216
subagent?: {
1317
/** Explicitly allow this plugin to request per-run provider/model overrides for subagent runs. */

src/config/zod-schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ const PluginEntrySchema = z
190190
.object({
191191
allowPromptInjection: z.boolean().optional(),
192192
allowConversationAccess: z.boolean().optional(),
193+
timeoutMs: z.number().int().positive().max(600_000).optional(),
194+
timeouts: z.record(z.string(), z.number().int().positive().max(600_000)).optional(),
193195
})
194196
.strict()
195197
.optional(),

0 commit comments

Comments
 (0)