Skip to content

Commit 5210d8b

Browse files
fix(compaction): correct timeout wording, document preflight gate, real-setup repro (#95553)
Address ClawSweeper P1 findings on PR #95675: - schema.help.ts:1518 - replace hardcoded '15-minute budget' wording with neutral description that references timeoutSeconds only (no specific default minutes). Keeps wording correct regardless of which default the merge target carries. - agent-runner-memory.ts comment - same neutral wording fix. - docs/gateway/config-agents.md - add preflight.enabled to the JSON5 example and bullet list under agents.defaults.compaction, matching the schema/help coverage. - scripts/repro/issue-95553-preflight-disabled-gate.mts - rewrite the repro to write a real openclaw.json to a temp dir and load it through the production loadConfig() so the gate value is parsed from disk, not cast from an inline literal. Same production path the gateway startup uses.
1 parent ffd17f3 commit 5210d8b

4 files changed

Lines changed: 91 additions & 63 deletions

File tree

docs/gateway/config-agents.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ Periodic heartbeat runs.
645645
model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override
646646
truncateAfterCompaction: true, // rotate to a smaller successor JSONL after compaction
647647
maxActiveTranscriptBytes: "20mb", // optional preflight local compaction trigger
648+
preflight: { enabled: false }, // optional gate that skips the pre-turn threshold check
648649
notifyUser: true, // send brief notices when compaction starts and completes (default: false)
649650
memoryFlush: {
650651
enabled: true,
@@ -670,6 +671,7 @@ Periodic heartbeat runs.
670671
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Reinjection is disabled when unset or set to `[]`. Explicitly setting `["Session Startup", "Red Lines"]` enables that pair and preserves the legacy `Every Session`/`Safety` fallback. Enable this only when the extra context is worth the risk of duplicating project guidance already captured in the compaction summary.
671672
- `model`: optional `provider/model-id` override for compaction summarization only. Use this when the main session should keep one model but compaction summaries should run on another; when unset, compaction uses the session's primary model.
672673
- `maxActiveTranscriptBytes`: optional byte threshold (`number` or strings like `"20mb"`) that triggers normal local compaction before a run when the active JSONL grows past the threshold. Requires `truncateAfterCompaction` so successful compaction can rotate to a smaller successor transcript. Disabled when unset or `0`.
674+
- `preflight`: optional gate around the pre-turn budget-triggered preflight compaction path. Set `preflight.enabled: false` to skip the pre-turn threshold check entirely and let every turn fall through to overflow recovery (which honors `timeoutSeconds`). Default: `preflight.enabled` is `true` and the existing preflight path runs as before.
673675
- `notifyUser`: when `true`, sends brief notices to the user when compaction starts and when it completes (for example, "Compacting context..." and "Compaction complete"). Disabled by default to keep compaction silent.
674676
- `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Set `model` to an exact provider/model such as `ollama/qwen3:8b` when this housekeeping turn should stay on a local model; the override does not inherit the active session fallback chain. Skipped when workspace is read-only.
675677

scripts/repro/issue-95553-preflight-disabled-gate.mts

Lines changed: 86 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,35 @@
1010
*
1111
* Run: pnpm exec tsx scripts/repro/issue-95553-preflight-disabled-gate.mts
1212
*
13-
* Behavior proved here:
14-
* 1. `preflight.enabled === false` short-circuits the preflight check and
15-
* `compactEmbeddedAgentSession` is NOT called.
16-
* 2. `preflight.enabled === true` proceeds to the preflight path (mock
17-
* resolves ok; gate does not interfere).
18-
* 3. `preflight` key absent preserves the existing default-on behavior
19-
* (preflight runs as before — the fix is strictly additive).
13+
* Behavior proved here (real-environment proof, not a unit-test mock):
14+
* 1. Writes a real `openclaw.json` to a temp state dir with
15+
* `agents.defaults.compaction.preflight.enabled: false`.
16+
* 2. Loads it via the production `loadConfig()` (same code path as gateway
17+
* startup) so the gate value is read from a parsed config, not a cast.
18+
* 3. Calls the production `runPreflightCompactionIfNeeded` with that
19+
* config plus a real on-disk session entry. The function short-circuits
20+
* before `compactEmbeddedAgentSession` is reached, returning the original
21+
* session entry unchanged.
2022
*
21-
* Real environment: this script runs against the real production
22-
* `runPreflightCompactionIfNeeded` function with vitest-style mocks for
23-
* `compactEmbeddedAgentSession`, `runEmbeddedAgent`, and `runWithModelFallback`.
24-
* The mocks are the same ones the unit tests use, so this script proves the
25-
* full production code path including the early-return gate.
23+
* Real environment: this script runs against the production
24+
* `runPreflightCompactionIfNeeded` function and the production config loader.
25+
* The only stub is `replyOperation` (an in-process callback bag with no
26+
* behavior exercised by the gate path).
2627
*/
2728
import assert from "node:assert/strict";
29+
import fs from "node:fs/promises";
30+
import os from "node:os";
31+
import path from "node:path";
32+
import { loadConfig } from "../../src/config/io.ts";
2833
import { runPreflightCompactionIfNeeded } from "../../src/auto-reply/reply/agent-runner-memory.ts";
34+
import { withEnvAsync } from "../../src/test-utils/env.ts";
2935

3036
type SessionEntry = Parameters<typeof runPreflightCompactionIfNeeded>[0]["sessionEntry"];
3137

32-
function makeSessionEntry(): SessionEntry {
38+
function makeSessionEntry(sessionFile: string): SessionEntry {
3339
return {
3440
sessionId: "session-95553-repro",
35-
sessionFile: "/tmp/openclaw-95553-repro/session.jsonl",
41+
sessionFile,
3642
updatedAt: Date.now(),
3743
totalTokens: 180_500,
3844
totalTokensFresh: true,
@@ -57,70 +63,90 @@ function makeReplyOperation(): Parameters<typeof runPreflightCompactionIfNeeded>
5763
}
5864

5965
async function main(): Promise<void> {
60-
const exitCode = 0;
61-
console.log("=== Reproduction for issue #95553 — preflight gate ===");
66+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-95553-repro-"));
67+
const configDir = path.join(tmpDir, "config");
68+
const sessionsDir = path.join(tmpDir, "sessions");
69+
await fs.mkdir(configDir, { recursive: true });
70+
await fs.mkdir(sessionsDir, { recursive: true });
71+
const configPath = path.join(configDir, "openclaw.json");
72+
const sessionFile = path.join(sessionsDir, "session.jsonl");
73+
const storePath = path.join(sessionsDir, "sessions.json");
6274

63-
// 1. preflight.enabled === false → no compactEmbeddedAgentSession call.
64-
// We rely on the unit test that proves this for the full path; here we
65-
// demonstrate the config-gate read is the only path that needs production
66-
// verification (no model resolution needed when gate is false).
67-
const cfg = {
75+
const openclawJson = {
6876
agents: {
6977
defaults: {
78+
model: "anthropic/claude-opus-4-6",
7079
compaction: {
7180
preflight: { enabled: false },
7281
},
7382
},
7483
},
7584
};
76-
const sessionEntry = makeSessionEntry();
77-
const replyOperation = makeReplyOperation();
85+
await fs.writeFile(configPath, JSON.stringify(openclawJson, null, 2), "utf8");
86+
await fs.writeFile(sessionFile, "", "utf8");
7887

79-
// The function returns synchronously when the gate is engaged and no
80-
// preflight work is needed; the gate is evaluated before any model lookup
81-
// or compactEmbeddedAgentSession call.
82-
const result = await runPreflightCompactionIfNeeded({
83-
cfg: cfg as never,
84-
followupRun: {
85-
run: {
86-
sessionId: sessionEntry.sessionId,
87-
sessionFile: sessionEntry.sessionFile,
88-
sessionKey: "agent:main:main",
89-
prompt: "x".repeat(5_000),
90-
model: "anthropic/claude-opus-4-6",
91-
provider: "anthropic",
92-
ownerNumbers: [],
88+
console.log("=== Reproduction for issue #95553 — preflight gate ===");
89+
console.log(`tmpDir: ${tmpDir}`);
90+
console.log(`openclaw.json: ${configPath}`);
91+
console.log(`sessionFile: ${sessionFile}`);
92+
93+
try {
94+
const cfg = await withEnvAsync(
95+
{
96+
OPENCLAW_CONFIG_PATH: configPath,
97+
OPENCLAW_STATE_DIR: tmpDir,
9398
},
94-
} as never,
95-
defaultModel: "anthropic/claude-opus-4-6",
96-
agentCfgContextTokens: 200_000,
97-
sessionEntry,
98-
sessionStore: { "agent:main:main": sessionEntry },
99-
sessionKey: "agent:main:main",
100-
storePath: "/tmp/openclaw-95553-repro/sessions.json",
101-
isHeartbeat: false,
102-
replyOperation,
103-
});
99+
async () => loadConfig({ pin: false }),
100+
);
101+
102+
const gateValue = cfg.agents?.defaults?.compaction?.preflight?.enabled;
103+
console.log(`loaded config: agents.defaults.compaction.preflight.enabled = ${gateValue}`);
104+
assert.equal(
105+
gateValue,
106+
false,
107+
"loaded config must carry preflight.enabled=false from openclaw.json",
108+
);
104109

105-
assert.equal(result, sessionEntry, "result must be the same session entry");
106-
console.log("PASS 1. preflight.enabled === false short-circuits preflight check");
107-
console.log(` sessionEntry returned unchanged: ${result?.sessionId}`);
110+
const sessionEntry = makeSessionEntry(sessionFile);
111+
const replyOperation = makeReplyOperation();
108112

109-
// 2-3 are covered by the unit test in
110-
// src/auto-reply/reply/agent-runner-memory.test.ts
111-
// ("skips preflight compaction when ... enabled === false") which
112-
// additionally verifies the negative path: that compactEmbeddedAgentSession
113-
// is never called and incrementCompactionCount is never called.
113+
const result = await runPreflightCompactionIfNeeded({
114+
cfg,
115+
followupRun: {
116+
run: {
117+
sessionId: sessionEntry.sessionId,
118+
sessionFile: sessionEntry.sessionFile,
119+
sessionKey: "agent:main:main",
120+
prompt: "x".repeat(5_000),
121+
model: "anthropic/claude-opus-4-6",
122+
provider: "anthropic",
123+
ownerNumbers: [],
124+
},
125+
} as never,
126+
defaultModel: "anthropic/claude-opus-4-6",
127+
agentCfgContextTokens: 200_000,
128+
sessionEntry,
129+
sessionStore: { "agent:main:main": sessionEntry },
130+
sessionKey: "agent:main:main",
131+
storePath,
132+
isHeartbeat: false,
133+
replyOperation,
134+
});
114135

115-
if (exitCode !== 0) {
116-
console.error("FAIL");
117-
} else {
136+
assert.equal(
137+
result,
138+
sessionEntry,
139+
"result must be the same session entry (preflight gate short-circuits)",
140+
);
141+
console.log("PASS preflight.enabled === false short-circuits preflight check");
142+
console.log(` sessionEntry returned unchanged: ${result?.sessionId}`);
118143
console.log("=== All repro assertions passed ===");
144+
} finally {
145+
await fs.rm(tmpDir, { recursive: true, force: true });
119146
}
120-
process.exit(exitCode);
121147
}
122148

123149
main().catch((err: unknown) => {
124150
console.error(err);
125151
process.exit(1);
126-
});
152+
});

src/auto-reply/reply/agent-runner-memory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,8 +713,8 @@ export async function runPreflightCompactionIfNeeded(params: {
713713
}
714714
// `compaction.preflight.enabled: false` short-circuits the budget-triggered
715715
// preflight path entirely; subsequent turns then rely on overflow recovery,
716-
// which already honors `compaction.timeoutSeconds` (~15min budget). The check
717-
// is `=== false` so an unset key preserves the existing default-on behavior.
716+
// which already honors `compaction.timeoutSeconds`. The check is `=== false`
717+
// so an unset key preserves the existing default-on behavior.
718718
if (params.cfg.agents?.defaults?.compaction?.preflight?.enabled === false) {
719719
return entry ?? params.sessionEntry;
720720
}

src/config/schema.help.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1515,7 +1515,7 @@ export const FIELD_HELP: Record<string, string> = {
15151515
"agents.defaults.compaction.preflight":
15161516
"Preflight (budget-triggered) compaction gate. Disable to skip the pre-turn threshold check entirely and let every turn fall through to overflow recovery (which honors `compaction.timeoutSeconds`).",
15171517
"agents.defaults.compaction.preflight.enabled":
1518-
"Enable preflight (budget-triggered) compaction. Default: true. Set false to skip preflight compaction; subsequent turns then rely on overflow recovery, which has its own 15-minute budget and is the only path that honors `compaction.timeoutSeconds` without being capped by the ~60s reply lifecycle.",
1518+
"Enable preflight (budget-triggered) compaction. Default: true. Set false to skip preflight compaction; subsequent turns then rely on overflow recovery, which honors `compaction.timeoutSeconds` without being capped by the reply lifecycle.",
15191519
"agents.defaults.runRetries":
15201520
"Outer run loop retry iteration boundaries for the embedded OpenClaw runner to prevent infinite execution loops during failure recovery.",
15211521
"agents.defaults.runRetries.base":

0 commit comments

Comments
 (0)