Skip to content

Commit 59449d7

Browse files
committed
fix(active-memory): make setup grace explicit
1 parent 98efae9 commit 59449d7

6 files changed

Lines changed: 84 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424

2525
### Fixes
2626

27+
- Active Memory: use the configured recall timeout as the blocking prompt-build hook budget by default and move cold-start setup grace behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently extends 15000 ms configs to 45000 ms on the main lane. Fixes #75843. Thanks @vishutdhar.
2728
- Agents/sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch. Fixes #44077. Thanks @patosullivan.
2829
- Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval.
2930
- Gateway/watch: keep colored subsystem log prefixes in the managed tmux pane even when the parent shell exports `NO_COLOR`, while preserving explicit `FORCE_COLOR=0` opt-out. Thanks @vincentkoc.

docs/concepts/active-memory.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -558,24 +558,25 @@ plugins.entries.active-memory
558558

559559
The most important fields are:
560560

561-
| Key | Type | Meaning |
562-
| --------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
563-
| `enabled` | `boolean` | Enables the plugin itself |
564-
| `config.agents` | `string[]` | Agent ids that may use active memory |
565-
| `config.model` | `string` | Optional blocking memory sub-agent model ref; when unset, active memory uses the current session model |
566-
| `config.allowedChatTypes` | `("direct" \| "group" \| "channel")[]` | Session types that may run Active Memory; defaults to direct-message style sessions |
567-
| `config.allowedChatIds` | `string[]` | Optional per-conversation allowlist applied after `allowedChatTypes`; non-empty lists fail closed |
568-
| `config.deniedChatIds` | `string[]` | Optional per-conversation denylist that overrides allowed session types and allowed ids |
569-
| `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory sub-agent sees |
570-
| `config.promptStyle` | `"balanced" \| "strict" \| "contextual" \| "recall-heavy" \| "precision-heavy" \| "preference-only"` | Controls how eager or strict the blocking memory sub-agent is when deciding whether to return memory |
571-
| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive" \| "max"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed |
572-
| `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use |
573-
| `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt |
574-
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms |
575-
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
576-
| `config.logging` | `boolean` | Emits active memory logs while tuning |
577-
| `config.persistTranscripts` | `boolean` | Keeps blocking memory sub-agent transcripts on disk instead of deleting temp files |
578-
| `config.transcriptDir` | `string` | Relative blocking memory sub-agent transcript directory under the agent sessions folder |
561+
| Key | Type | Meaning |
562+
| ---------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
563+
| `enabled` | `boolean` | Enables the plugin itself |
564+
| `config.agents` | `string[]` | Agent ids that may use active memory |
565+
| `config.model` | `string` | Optional blocking memory sub-agent model ref; when unset, active memory uses the current session model |
566+
| `config.allowedChatTypes` | `("direct" \| "group" \| "channel")[]` | Session types that may run Active Memory; defaults to direct-message style sessions |
567+
| `config.allowedChatIds` | `string[]` | Optional per-conversation allowlist applied after `allowedChatTypes`; non-empty lists fail closed |
568+
| `config.deniedChatIds` | `string[]` | Optional per-conversation denylist that overrides allowed session types and allowed ids |
569+
| `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory sub-agent sees |
570+
| `config.promptStyle` | `"balanced" \| "strict" \| "contextual" \| "recall-heavy" \| "precision-heavy" \| "preference-only"` | Controls how eager or strict the blocking memory sub-agent is when deciding whether to return memory |
571+
| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive" \| "max"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed |
572+
| `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use |
573+
| `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt |
574+
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms |
575+
| `config.setupGraceTimeoutMs` | `number` | Advanced extra setup budget before the recall timeout expires; defaults to 0 and is capped at 30000 ms |
576+
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
577+
| `config.logging` | `boolean` | Emits active memory logs while tuning |
578+
| `config.persistTranscripts` | `boolean` | Keeps blocking memory sub-agent transcripts on disk instead of deleting temp files |
579+
| `config.transcriptDir` | `string` | Relative blocking memory sub-agent transcript directory under the agent sessions folder |
579580

580581
Useful tuning fields:
581582

extensions/active-memory/config.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ describe("active-memory manifest config schema", () => {
3636
expect(result.ok).toBe(true);
3737
});
3838

39+
it("accepts setupGraceTimeoutMs values at the runtime ceiling", () => {
40+
const result = validateJsonSchemaValue({
41+
schema: manifest.configSchema,
42+
cacheKey: "active-memory.manifest.setup-grace-timeout-ceiling",
43+
value: {
44+
enabled: true,
45+
agents: ["main"],
46+
setupGraceTimeoutMs: 30_000,
47+
},
48+
});
49+
50+
expect(result.ok).toBe(true);
51+
});
52+
3953
it("accepts explicit in allowedChatTypes", () => {
4054
const result = validateJsonSchemaValue({
4155
schema: manifest.configSchema,
@@ -64,6 +78,20 @@ describe("active-memory manifest config schema", () => {
6478
expect(result.ok).toBe(false);
6579
});
6680

81+
it("rejects setupGraceTimeoutMs values above the runtime ceiling", () => {
82+
const result = validateJsonSchemaValue({
83+
schema: manifest.configSchema,
84+
cacheKey: "active-memory.manifest.setup-grace-timeout-above-ceiling",
85+
value: {
86+
enabled: true,
87+
agents: ["main"],
88+
setupGraceTimeoutMs: 30_001,
89+
},
90+
});
91+
92+
expect(result.ok).toBe(false);
93+
});
94+
6795
it("rejects unknown allowedChatTypes values", () => {
6896
const result = validateJsonSchemaValue({
6997
schema: manifest.configSchema,

extensions/active-memory/index.test.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,18 +185,29 @@ describe("active-memory plugin", () => {
185185

186186
it("registers a before_prompt_build hook", () => {
187187
expect(api.on).toHaveBeenCalledWith("before_prompt_build", expect.any(Function), {
188-
timeoutMs: 45_000,
188+
timeoutMs: 15_000,
189189
});
190-
expect(hookOptions.before_prompt_build?.timeoutMs).toBe(45_000);
190+
expect(hookOptions.before_prompt_build?.timeoutMs).toBe(15_000);
191191
});
192192

193-
it("registers before_prompt_build with the configured recall timeout plus setup grace", () => {
193+
it("registers before_prompt_build with the configured recall timeout", () => {
194194
api.pluginConfig = {
195195
agents: ["main"],
196196
timeoutMs: 90_000,
197197
};
198198
plugin.register(api as unknown as OpenClawPluginApi);
199199

200+
expect(hookOptions.before_prompt_build?.timeoutMs).toBe(90_000);
201+
});
202+
203+
it("registers before_prompt_build with explicit setup grace when configured", () => {
204+
api.pluginConfig = {
205+
agents: ["main"],
206+
timeoutMs: 90_000,
207+
setupGraceTimeoutMs: 30_000,
208+
};
209+
plugin.register(api as unknown as OpenClawPluginApi);
210+
200211
expect(hookOptions.before_prompt_build?.timeoutMs).toBe(120_000);
201212
});
202213

@@ -2178,10 +2189,10 @@ describe("active-memory plugin", () => {
21782189
it("does not spend the model timeout budget on active-memory subagent setup", async () => {
21792190
const CONFIGURED_TIMEOUT_MS = 10;
21802191
__testing.setMinimumTimeoutMsForTests(1);
2181-
__testing.setSetupGraceTimeoutMsForTests(100);
21822192
api.pluginConfig = {
21832193
agents: ["main"],
21842194
timeoutMs: CONFIGURED_TIMEOUT_MS,
2195+
setupGraceTimeoutMs: 100,
21852196
logging: true,
21862197
};
21872198
plugin.register(api as unknown as OpenClawPluginApi);
@@ -3242,6 +3253,16 @@ describe("active-memory plugin", () => {
32423253
expect(config.circuitBreakerCooldownMs).toBe(60_000);
32433254
});
32443255

3256+
it("normalizes setup grace config with a zero default and bounded opt-in", () => {
3257+
expect(__testing.normalizePluginConfig({}).setupGraceTimeoutMs).toBe(0);
3258+
expect(
3259+
__testing.normalizePluginConfig({ setupGraceTimeoutMs: 30_001 }).setupGraceTimeoutMs,
3260+
).toBe(30_000);
3261+
expect(__testing.normalizePluginConfig({ setupGraceTimeoutMs: -1 }).setupGraceTimeoutMs).toBe(
3262+
0,
3263+
);
3264+
});
3265+
32453266
it("clamps circuit breaker config within valid ranges", () => {
32463267
const config = __testing.normalizePluginConfig({
32473268
circuitBreakerMaxTimeouts: 0,

extensions/active-memory/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const DEFAULT_CACHE_TTL_MS = 15_000;
3535
const DEFAULT_MAX_CACHE_ENTRIES = 1000;
3636
const CACHE_SWEEP_INTERVAL_MS = 1000;
3737
const DEFAULT_MIN_TIMEOUT_MS = 250;
38-
const DEFAULT_SETUP_GRACE_TIMEOUT_MS = 30_000;
38+
const DEFAULT_SETUP_GRACE_TIMEOUT_MS = 0;
3939
const DEFAULT_QUERY_MODE = "recent" as const;
4040
const DEFAULT_QMD_SEARCH_MODE = "search" as const;
4141
const DEFAULT_TRANSCRIPT_DIR = "active-memory";
@@ -91,6 +91,7 @@ type ActiveRecallPluginConfig = {
9191
promptOverride?: string;
9292
promptAppend?: string;
9393
timeoutMs?: number;
94+
setupGraceTimeoutMs?: number;
9495
queryMode?: "message" | "recent" | "full";
9596
maxSummaryChars?: number;
9697
recentUserTurns?: number;
@@ -130,6 +131,7 @@ type ResolvedActiveRecallPluginConfig = {
130131
promptOverride?: string;
131132
promptAppend?: string;
132133
timeoutMs: number;
134+
setupGraceTimeoutMs: number;
133135
queryMode: "message" | "recent" | "full";
134136
maxSummaryChars: number;
135137
recentUserTurns: number;
@@ -746,6 +748,7 @@ function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPlugi
746748
minimumTimeoutMs,
747749
120_000,
748750
),
751+
setupGraceTimeoutMs: clampInt(raw.setupGraceTimeoutMs, setupGraceTimeoutMs, 0, 30_000),
749752
queryMode:
750753
raw.queryMode === "message" || raw.queryMode === "recent" || raw.queryMode === "full"
751754
? raw.queryMode
@@ -2280,7 +2283,7 @@ async function maybeResolveActiveRecall(params: {
22802283
const controller = new AbortController();
22812284
const TIMEOUT_SENTINEL = Symbol("timeout");
22822285
let sessionFile: string | undefined;
2283-
const watchdogTimeoutMs = params.config.timeoutMs + setupGraceTimeoutMs;
2286+
const watchdogTimeoutMs = params.config.timeoutMs + params.config.setupGraceTimeoutMs;
22842287
const timeoutId = setTimeout(() => {
22852288
controller.abort(new Error(`active-memory timeout after ${watchdogTimeoutMs}ms`));
22862289
}, watchdogTimeoutMs);
@@ -2535,7 +2538,7 @@ export default definePluginEntry({
25352538
},
25362539
});
25372540

2538-
const beforePromptBuildTimeoutMs = config.timeoutMs + setupGraceTimeoutMs;
2541+
const beforePromptBuildTimeoutMs = config.timeoutMs + config.setupGraceTimeoutMs;
25392542
api.on(
25402543
"before_prompt_build",
25412544
async (event, ctx) => {

extensions/active-memory/openclaw.plugin.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"enum": ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"]
4141
},
4242
"timeoutMs": { "type": "integer", "minimum": 250, "maximum": 120000 },
43+
"setupGraceTimeoutMs": { "type": "integer", "minimum": 0, "maximum": 30000 },
4344
"queryMode": {
4445
"type": "string",
4546
"enum": ["message", "recent", "full"]
@@ -116,6 +117,10 @@
116117
"timeoutMs": {
117118
"label": "Timeout (ms)"
118119
},
120+
"setupGraceTimeoutMs": {
121+
"label": "Setup Grace Timeout (ms)",
122+
"help": "Advanced: extra blocking budget for cold embedded-run setup before the recall timeout is considered exhausted. Defaults to 0 so timeoutMs remains the main-lane hook budget unless you opt in."
123+
},
119124
"queryMode": {
120125
"label": "Query Mode",
121126
"help": "Choose whether the blocking memory sub-agent sees only the latest user message, a small recent tail, or the full conversation."

0 commit comments

Comments
 (0)