Skip to content

Commit ab910f8

Browse files
authored
fix: make compaction reinjection opt-in
Summary: - Make post-compaction AGENTS.md reinjection explicit opt-in for configured sections. - Carry the run workspace into compaction-safeguard AGENTS.md reads. - Improve collapsed Control UI tool rows while preserving raw expanded tool details. Verification: - CI green on PR head 9610166. - pnpm exec oxfmt --check --threads=1 <changed files> - OPENCLAW_OXLINT_SKIP_PREPARE=1 node scripts/run-oxlint.mjs <changed ts/mjs files> - node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo - git diff --check origin/main...HEAD && git diff --check - node scripts/run-vitest.mjs src/agents/pi-hooks/compaction-safeguard.test.ts src/agents/pi-embedded-runner/extensions.test.ts -t "workspace" - node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts -t "reads opted-in post-compaction context" - node scripts/run-vitest.mjs test/scripts/test-projects.test.ts -t "allows explicit split Vitest config targets" - node scripts/run-vitest.mjs ui/src/ui/chat/tool-cards.test.ts ui/src/ui/chat/tool-cards.node.test.ts ui/src/ui/chat/grouped-render.test.ts ui/src/styles/chat/tool-cards.test.ts - AUTOREVIEW_AUTO_TESTS=0 .agents/skills/autoreview/scripts/autoreview --mode branch Fixes #45488. Fixes #45649. Supersedes #67090.
1 parent c3c8a65 commit ab910f8

24 files changed

Lines changed: 469 additions & 104 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
7171
- Live tests: fail Gateway live model sweeps when selected coverage is lost to timeouts or stale high-signal filters instead of reporting false missing-profile coverage, and pin Docker OpenAI gateway coverage to the current `gpt-5.5` lane.
7272
- Tests: fail Docker resource-ceiling checks when stats samples or configured limits are invalid instead of silently reporting zero peaks.
7373
- Agents: fail closed when provider-less session models match multiple provider-prefixed runtime policies so CLI runtime routing no longer depends on config order. (#85970) Thanks @potterdigital.
74+
- Control UI/agents: keep collapsed tool rows readable without early ellipses, preserve raw expanded tool details, and make post-compaction AGENTS.md reinjection opt-in to avoid duplicated project context. Fixes #45649 and #45488. Thanks @BunsDev.
7475

7576
## 2026.5.24
7677

docs/gateway/config-agents.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ Periodic heartbeat runs.
652652
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
653653
qualityGuard: { enabled: true, maxRetries: 1 },
654654
midTurnPrecheck: { enabled: false }, // optional Pi tool-loop pressure check
655-
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
655+
postCompactionSections: ["Session Startup", "Red Lines"], // opt in to AGENTS.md section reinjection
656656
model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override
657657
truncateAfterCompaction: true, // rotate to a smaller successor JSONL after compaction
658658
maxActiveTranscriptBytes: "20mb", // optional preflight local compaction trigger
@@ -678,7 +678,7 @@ Periodic heartbeat runs.
678678
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
679679
- `qualityGuard`: retry-on-malformed-output checks for safeguard summaries. Enabled by default in safeguard mode; set `enabled: false` to skip the audit.
680680
- `midTurnPrecheck`: optional Pi tool-loop pressure check. When `enabled: true`, OpenClaw checks context pressure after tool results are appended and before the next model call. If the context no longer fits, it aborts the current attempt before submitting the prompt and reuses the existing precheck recovery path to truncate tool results or compact and retry. Works with both `default` and `safeguard` compaction modes. Default: disabled.
681-
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Defaults to `["Session Startup", "Red Lines"]`; set `[]` to disable reinjection. When unset or explicitly set to that default pair, older `Every Session`/`Safety` headings are also accepted as a legacy fallback.
681+
- `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.
682682
- `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.
683683
- `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`.
684684
- `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.

docs/reference/session-management-compaction.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ After compaction, future turns see:
248248
- The compaction summary
249249
- Messages after `firstKeptEntryId`
250250

251+
AGENTS.md section reinjection after compaction is opt-in via
252+
`agents.defaults.compaction.postCompactionSections`; when unset or `[]`,
253+
OpenClaw does not append AGENTS.md excerpts on top of the compaction summary.
254+
251255
Compaction is **persistent** (unlike session pruning). See [/concepts/session-pruning](/concepts/session-pruning).
252256

253257
## Compaction chunk boundaries and tool pairing

docs/reference/token-use.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ OpenClaw assembles its own system prompt on every run. It includes:
1919
with optional per-agent override at
2020
`agents.list[].skillsLimits.maxSkillsPromptChars`.
2121
- Self-update instructions
22-
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` when present). Lowercase root `memory.md` is not injected; it is legacy repair input for `openclaw doctor --fix` when paired with `MEMORY.md`. Large files are truncated by `agents.defaults.bootstrapMaxChars` (default: 12000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `memory/*.md` daily files are not part of the normal bootstrap prompt; they remain on-demand via memory tools on ordinary turns, but reset/startup model runs can prepend a one-shot startup-context block with recent daily memory for that first turn. Bare chat `/new` and `/reset` commands are acknowledged without invoking the model. The startup prelude is controlled by `agents.defaults.startupContext`.
22+
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` when present). Lowercase root `memory.md` is not injected; it is legacy repair input for `openclaw doctor --fix` when paired with `MEMORY.md`. Large files are truncated by `agents.defaults.bootstrapMaxChars` (default: 12000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `memory/*.md` daily files are not part of the normal bootstrap prompt; they remain on-demand via memory tools on ordinary turns, but reset/startup model runs can prepend a one-shot startup-context block with recent daily memory for that first turn. Bare chat `/new` and `/reset` commands are acknowledged without invoking the model. The startup prelude is controlled by `agents.defaults.startupContext`. Post-compaction AGENTS.md excerpts are separate and require explicit `agents.defaults.compaction.postCompactionSections` opt-in.
2323
- Time (UTC + user timezone)
2424
- Reply tags + heartbeat behavior
2525
- Runtime metadata (host/OS/model/thinking)

src/agents/pi-embedded-runner/compact.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,7 @@ async function compactEmbeddedPiSessionDirectOnce(
10351035
const extensionFactories = buildEmbeddedExtensionFactories({
10361036
cfg: params.config,
10371037
sessionManager,
1038+
workspaceDir: effectiveWorkspace,
10381039
provider,
10391040
modelId,
10401041
model,

src/agents/pi-embedded-runner/extensions.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ vi.mock("../../plugins/provider-hook-runtime.js", () => ({
1616
resolveProviderRuntimePlugin: () => undefined,
1717
}));
1818

19-
function buildSafeguardFactories(cfg: OpenClawConfig) {
19+
function buildSafeguardFactories(cfg: OpenClawConfig, workspaceDir?: string) {
2020
const sessionManager = {} as SessionManager;
2121
const model = {
2222
id: "claude-sonnet-4-20250514",
@@ -26,6 +26,7 @@ function buildSafeguardFactories(cfg: OpenClawConfig) {
2626
const factories = buildEmbeddedExtensionFactories({
2727
cfg,
2828
sessionManager,
29+
workspaceDir,
2930
provider: "anthropic",
3031
modelId: "claude-sonnet-4-20250514",
3132
model,
@@ -101,6 +102,25 @@ describe("buildEmbeddedExtensionFactories", () => {
101102
});
102103
});
103104

105+
it("wires the run workspace into safeguard runtime", () => {
106+
const { sessionManager } = buildSafeguardFactories(
107+
{
108+
agents: {
109+
defaults: {
110+
compaction: {
111+
mode: "safeguard",
112+
},
113+
},
114+
},
115+
} as OpenClawConfig,
116+
"/tmp/openclaw-workspace",
117+
);
118+
119+
expect(getCompactionSafeguardRuntime(sessionManager)?.workspaceDir).toBe(
120+
"/tmp/openclaw-workspace",
121+
);
122+
});
123+
104124
it("enables cache-ttl pruning for custom anthropic-messages providers", () => {
105125
const factories = buildEmbeddedExtensionFactories({
106126
cfg: {

src/agents/pi-embedded-runner/extensions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ function buildContextPruningFactory(params: {
141141
export function buildEmbeddedExtensionFactories(params: {
142142
cfg: OpenClawConfig | undefined;
143143
sessionManager: SessionManager;
144+
workspaceDir?: string;
144145
provider: string;
145146
modelId: string;
146147
model: ProviderRuntimeModel | undefined;
@@ -167,6 +168,8 @@ export function buildEmbeddedExtensionFactories(params: {
167168
qualityGuardMaxRetries: qualityGuardCfg?.maxRetries,
168169
model: params.model,
169170
recentTurnsPreserve: compactionCfg?.recentTurnsPreserve,
171+
workspaceDir: params.workspaceDir,
172+
postCompactionSections: compactionCfg?.postCompactionSections,
170173
provider: compactionCfg?.provider,
171174
});
172175
factories.push(compactionSafeguardExtension);

src/agents/pi-embedded-runner/run/attempt.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,7 @@ export async function runEmbeddedAttempt(
22262226
const extensionFactories = buildEmbeddedExtensionFactories({
22272227
cfg: params.config,
22282228
sessionManager,
2229+
workspaceDir: effectiveWorkspace,
22292230
provider: params.provider,
22302231
modelId: params.modelId,
22312232
model: params.model,

src/agents/pi-hooks/compaction-safeguard-runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export type CompactionSafeguardRuntimeValue = {
1515
*/
1616
model?: Model<Api>;
1717
recentTurnsPreserve?: number;
18+
workspaceDir?: string;
19+
postCompactionSections?: string[];
1820
qualityGuardEnabled?: boolean;
1921
qualityGuardMaxRetries?: number;
2022
/**

src/agents/pi-hooks/compaction-safeguard.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2370,14 +2370,93 @@ async function expectWorkspaceSummaryEmptyForAgentsAlias(
23702370
const outside = path.join(root, "outside-secret.txt");
23712371
fs.writeFileSync(outside, "secret");
23722372
createAlias(outside, path.join(root, "AGENTS.md"));
2373-
await expect(readWorkspaceContextForSummary()).resolves.toBe("");
2373+
await expect(readWorkspaceContextForSummary(["Session Startup", "Red Lines"])).resolves.toBe(
2374+
"",
2375+
);
23742376
} finally {
23752377
cwdSpy.mockRestore();
23762378
fs.rmSync(root, { recursive: true, force: true });
23772379
}
23782380
}
23792381

23802382
describe("readWorkspaceContextForSummary", () => {
2383+
async function withWorkspaceSummary(
2384+
content: string,
2385+
sectionNames: string[] | undefined,
2386+
): Promise<string> {
2387+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-compaction-summary-"));
2388+
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(root);
2389+
try {
2390+
fs.writeFileSync(path.join(root, "AGENTS.md"), content);
2391+
return await readWorkspaceContextForSummary(sectionNames);
2392+
} finally {
2393+
cwdSpy.mockRestore();
2394+
fs.rmSync(root, { recursive: true, force: true });
2395+
}
2396+
}
2397+
2398+
it("returns empty when post-compaction sections are not configured", async () => {
2399+
const result = await withWorkspaceSummary(
2400+
"## Session Startup\n\nRead AGENTS.md\n\n## Red Lines\n\nBe careful.\n",
2401+
undefined,
2402+
);
2403+
2404+
expect(result).toBe("");
2405+
});
2406+
2407+
it("returns empty when post-compaction sections are explicitly disabled", async () => {
2408+
const result = await withWorkspaceSummary("## Session Startup\n\nRead AGENTS.md\n", []);
2409+
2410+
expect(result).toBe("");
2411+
});
2412+
2413+
it("injects workspace critical rules only for explicit section opt-in", async () => {
2414+
const result = await withWorkspaceSummary(
2415+
"## Session Startup\n\nRead AGENTS.md\n\n## Other\n\nIgnore me.\n",
2416+
["Session Startup", "Red Lines"],
2417+
);
2418+
2419+
expect(result).toContain("<workspace-critical-rules>");
2420+
expect(result).toContain("## Session Startup");
2421+
expect(result).toContain("Read AGENTS.md");
2422+
expect(result).not.toContain("Ignore me");
2423+
});
2424+
2425+
it("reads workspace context from the configured workspace instead of process cwd", async () => {
2426+
const processRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-compaction-cwd-"));
2427+
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-compaction-workspace-"));
2428+
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(processRoot);
2429+
try {
2430+
fs.writeFileSync(
2431+
path.join(processRoot, "AGENTS.md"),
2432+
"## Session Startup\n\nWrong cwd rules.\n",
2433+
);
2434+
fs.writeFileSync(
2435+
path.join(workspaceRoot, "AGENTS.md"),
2436+
"## Session Startup\n\nUse the run workspace rules.\n",
2437+
);
2438+
2439+
const result = await readWorkspaceContextForSummary(["Session Startup"], workspaceRoot);
2440+
2441+
expect(result).toContain("Use the run workspace rules.");
2442+
expect(result).not.toContain("Wrong cwd rules.");
2443+
} finally {
2444+
cwdSpy.mockRestore();
2445+
fs.rmSync(processRoot, { recursive: true, force: true });
2446+
fs.rmSync(workspaceRoot, { recursive: true, force: true });
2447+
}
2448+
});
2449+
2450+
it("preserves legacy fallback only for the explicit default section pair", async () => {
2451+
const result = await withWorkspaceSummary(
2452+
"## Every Session\n\nDo startup things.\n\n## Safety\n\nBe safe.\n",
2453+
["Red Lines", "Session Startup"],
2454+
);
2455+
2456+
expect(result).toContain("Do startup things");
2457+
expect(result).toContain("Be safe");
2458+
});
2459+
23812460
it.runIf(process.platform !== "win32")(
23822461
"returns empty when AGENTS.md is a symlink escape",
23832462
async () => {

0 commit comments

Comments
 (0)