Skip to content

fix(compaction): resolve model override in runtime context for all context engines#56710

Merged
jalehman merged 6 commits into
openclaw:mainfrom
oliviareid-svg:fix/compaction-model-override-56649
Mar 31, 2026
Merged

fix(compaction): resolve model override in runtime context for all context engines#56710
jalehman merged 6 commits into
openclaw:mainfrom
oliviareid-svg:fix/compaction-model-override-56649

Conversation

@oliviareid-svg

Copy link
Copy Markdown
Contributor

Summary

  • The agents.defaults.compaction.model config override was only resolved inside compactEmbeddedPiSessionDirect (compact.ts), which is only reached by the legacy context engine's compact() via delegateCompactionToRuntime.
  • Custom context engines with ownsCompaction=true that implement their own compact() never saw the override, causing overflow and timeout recovery to use the session's default model.
  • Fix: move the override resolution into buildEmbeddedCompactionRuntimeContext so the runtime context always carries the correct compaction provider/model regardless of which context engine handles the compact() call.
  • When the override changes the provider, the auth profile is dropped to avoid sending wrong credentials (same logic as in compact.ts).

Test plan

  • Added 3 new tests in compaction-runtime-context.test.ts covering override with provider/model format, model-only format, and no-override fallback
  • Updated existing test in attempt.test.ts to expect the override to be resolved eagerly in the runtime context

Fixes #56649

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Mar 29, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4e9b57a9c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const slashIdx = override.indexOf("/");
if (slashIdx > 0) {
const overrideProvider = override.slice(0, slashIdx).trim();
const overrideModel = override.slice(slashIdx + 1).trim() || undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fallback model when override omits model id

When agents.defaults.compaction.model is configured as provider/ (or provider/ ), resolveCompactionModel now sets model to undefined via trim() || undefined. That is a regression for engine-owned compaction paths, because these engines consume runtimeContext.model directly and can skip/fail compaction when it is missing; the legacy runtime path previously tolerated this by falling back to a default model in compactEmbeddedPiSessionDirect. Aligning this branch with the existing fallback behavior avoids turning a recoverable config typo into compaction failure.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a real bug where the agents.defaults.compaction.model config override was only applied inside compactEmbeddedPiSessionDirect, making it invisible to custom context engines that implement their own compact() with ownsCompaction=true. The fix correctly moves the override resolution into buildEmbeddedCompactionRuntimeContext, so the runtime context is always populated with the correct compaction provider, model, and auth profile before it reaches any context engine — including the overflow and after-turn paths in run.ts and attempt.ts.

Key observations:

  • The fix is logically correct. For the legacy path (delegateCompactionToRuntimecompactEmbeddedPiSessionDirect), the override in compact.ts is re-applied on already-resolved values; this is idempotent and harmless, though it is duplicated logic that should be consolidated in a follow-up.
  • There is a minor fallback divergence: resolveCompactionModel returns model: undefined for a malformed "provider/" override string, while the parallel code in compact.ts substitutes DEFAULT_MODEL. Custom engines would observe undefined; the legacy path self-corrects. Very unlikely to matter in practice.
  • attempt.test.ts includes several unrelated new test suites (sessions_yield helpers, shouldInjectHeartbeatPrompt, resolveEmbeddedAgentStreamFn) that are out-of-scope for the stated fix; consider splitting them into a separate PR for cleaner history.
  • New unit tests in compaction-runtime-context.test.ts adequately cover the three override shapes.

Confidence Score: 5/5

Safe to merge — the core fix is correct, the double-resolution in the legacy path is idempotent, and all remaining findings are P2 style/cleanup concerns.

All findings are P2 (duplicate logic, minor edge-case fallback divergence, unrelated test additions). No P0/P1 issues found. The fix is narrowly scoped and well-tested.

No files require special attention, though compaction-runtime-context.ts and compact.ts carry duplicated override-parsing logic that should be consolidated as follow-up.

Important Files Changed

Filename Overview
src/agents/pi-embedded-runner/compaction-runtime-context.ts Core fix: adds resolveCompactionModel and calls it inside buildEmbeddedCompactionRuntimeContext so every consumer receives the override-resolved provider/model/authProfileId. Logic is correct and the double-resolution in the legacy path is idempotent, but there is a minor fallback divergence from compact.ts for the malformed 'provider/' edge case.
src/agents/pi-embedded-runner/compaction-runtime-context.test.ts Three new unit tests cover the three main override shapes (provider/model, model-only, no override) with correct assertions including the auth-profile-drop behaviour on provider change.
src/agents/pi-embedded-runner/run/attempt.test.ts Updates the existing compaction override test to expect the override to be eagerly resolved in the runtime context (correct). Also includes model-version fixture bumps and several unrelated new test suites. No correctness issues found.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/compaction-runtime-context.ts
Line: 51-60

Comment:
**Duplicate override resolution logic**

`resolveCompactionModel` here and the equivalent block in `compactEmbeddedPiSessionDirect` (`compact.ts` lines 410–427) are now two independent copies of the same parsing logic. Because `delegateCompactionToRuntime` spreads the pre-resolved runtime context directly into `compactEmbeddedPiSessionDirect`, the override in `compact.ts` is re-applied on top of values that already reflect the override — it is idempotent in that path, but it's dead/redundant work and a maintenance liability. If the parsing semantics ever diverge, the legacy and custom-engine paths will silently produce different results.

Consider extracting the shared parsing into a small exported helper (e.g. `parseCompactionModelOverride`) that both sites import, so there is one source of truth. This doesn't need to block the PR, but it's worth tracking as follow-up.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/compaction-runtime-context.ts
Line: 53

Comment:
**Empty-model fallback diverges from `compact.ts`**

When the override string ends with a slash (e.g. `"anthropic/"`), this line yields `undefined`:

```ts
const overrideModel = override.slice(slashIdx + 1).trim() || undefined;
```

whereas the parallel code in `compactEmbeddedPiSessionDirect` falls back to `DEFAULT_MODEL`:

```ts
modelId = compactionModelOverride.slice(slashIdx + 1).trim() || DEFAULT_MODEL;
```

For the legacy path the final value is `DEFAULT_MODEL` (the override in `compact.ts` corrects it). For a custom context engine with `ownsCompaction=true` the runtime context it receives will have `model: undefined`, which the engine must handle itself. The input is almost certainly invalid, but a consistent fallback between both sites would make the behaviour more predictable.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.test.ts
Line: 153-250

Comment:
**Unrelated test additions in a focused compaction PR**

The `sessions_yield helpers`, `shouldInjectHeartbeatPrompt`, and `resolveEmbeddedAgentStreamFn` test suites added here are unrelated to the compaction model override fix described in the PR. Bundling unrelated test additions makes the diff harder to review and muddies the git history. Consider splitting them into a separate PR or commit if the team's convention is to keep PRs focused. Not a correctness issue — just a scope observation.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "test(compaction): update attempt test fo..." | Re-trigger Greptile

Comment on lines +51 to +60
if (slashIdx > 0) {
const overrideProvider = override.slice(0, slashIdx).trim();
const overrideModel = override.slice(slashIdx + 1).trim() || undefined;
// When switching provider via override, drop the primary auth profile to
// avoid sending the wrong credentials.
const authProfileId =
overrideProvider !== (params.provider ?? "")?.trim()
? undefined
: (params.authProfileId ?? undefined);
return { provider: overrideProvider, model: overrideModel, authProfileId };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate override resolution logic

resolveCompactionModel here and the equivalent block in compactEmbeddedPiSessionDirect (compact.ts lines 410–427) are now two independent copies of the same parsing logic. Because delegateCompactionToRuntime spreads the pre-resolved runtime context directly into compactEmbeddedPiSessionDirect, the override in compact.ts is re-applied on top of values that already reflect the override — it is idempotent in that path, but it's dead/redundant work and a maintenance liability. If the parsing semantics ever diverge, the legacy and custom-engine paths will silently produce different results.

Consider extracting the shared parsing into a small exported helper (e.g. parseCompactionModelOverride) that both sites import, so there is one source of truth. This doesn't need to block the PR, but it's worth tracking as follow-up.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/compaction-runtime-context.ts
Line: 51-60

Comment:
**Duplicate override resolution logic**

`resolveCompactionModel` here and the equivalent block in `compactEmbeddedPiSessionDirect` (`compact.ts` lines 410–427) are now two independent copies of the same parsing logic. Because `delegateCompactionToRuntime` spreads the pre-resolved runtime context directly into `compactEmbeddedPiSessionDirect`, the override in `compact.ts` is re-applied on top of values that already reflect the override — it is idempotent in that path, but it's dead/redundant work and a maintenance liability. If the parsing semantics ever diverge, the legacy and custom-engine paths will silently produce different results.

Consider extracting the shared parsing into a small exported helper (e.g. `parseCompactionModelOverride`) that both sites import, so there is one source of truth. This doesn't need to block the PR, but it's worth tracking as follow-up.

How can I resolve this? If you propose a fix, please make it concise.

const slashIdx = override.indexOf("/");
if (slashIdx > 0) {
const overrideProvider = override.slice(0, slashIdx).trim();
const overrideModel = override.slice(slashIdx + 1).trim() || undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty-model fallback diverges from compact.ts

When the override string ends with a slash (e.g. "anthropic/"), this line yields undefined:

const overrideModel = override.slice(slashIdx + 1).trim() || undefined;

whereas the parallel code in compactEmbeddedPiSessionDirect falls back to DEFAULT_MODEL:

modelId = compactionModelOverride.slice(slashIdx + 1).trim() || DEFAULT_MODEL;

For the legacy path the final value is DEFAULT_MODEL (the override in compact.ts corrects it). For a custom context engine with ownsCompaction=true the runtime context it receives will have model: undefined, which the engine must handle itself. The input is almost certainly invalid, but a consistent fallback between both sites would make the behaviour more predictable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/compaction-runtime-context.ts
Line: 53

Comment:
**Empty-model fallback diverges from `compact.ts`**

When the override string ends with a slash (e.g. `"anthropic/"`), this line yields `undefined`:

```ts
const overrideModel = override.slice(slashIdx + 1).trim() || undefined;
```

whereas the parallel code in `compactEmbeddedPiSessionDirect` falls back to `DEFAULT_MODEL`:

```ts
modelId = compactionModelOverride.slice(slashIdx + 1).trim() || DEFAULT_MODEL;
```

For the legacy path the final value is `DEFAULT_MODEL` (the override in `compact.ts` corrects it). For a custom context engine with `ownsCompaction=true` the runtime context it receives will have `model: undefined`, which the engine must handle itself. The input is almost certainly invalid, but a consistent fallback between both sites would make the behaviour more predictable.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 153 to 250
});
});

describe("sessions_yield helpers", () => {
it("builds a hidden follow-up context note", () => {
expect(buildSessionsYieldContextMessage("Waiting for subagent")).toContain(
"Waiting for subagent",
);
expect(buildSessionsYieldContextMessage("Waiting for subagent")).toContain(
"ended intentionally via sessions_yield",
);
});

it("queues a hidden interrupt steering message", () => {
const steer = vi.fn();
queueSessionsYieldInterruptMessage({ agent: { steer } });
expect(steer).toHaveBeenCalledWith(
expect.objectContaining({
role: "custom",
customType: "openclaw.sessions_yield_interrupt",
display: false,
details: { source: "sessions_yield" },
}),
);
});

it("persists a hidden yield context message without triggering a turn", async () => {
const sendCustomMessage = vi.fn(async () => {});
await persistSessionsYieldContextMessage(
{
sendCustomMessage,
},
"Waiting for subagent",
);
expect(sendCustomMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "openclaw.sessions_yield",
display: false,
details: { source: "sessions_yield", message: "Waiting for subagent" },
content: expect.stringContaining("Waiting for subagent"),
}),
{ triggerTurn: false },
);
});

it("strips trailing yield interrupt artifacts from memory and transcript state", () => {
const replaceMessages = vi.fn();
const rewriteFile = vi.fn();
const activeSession = {
messages: [
{ role: "user", content: [{ type: "text", text: "hi" }] },
{ role: "custom", customType: "openclaw.sessions_yield_interrupt" },
{ role: "assistant", stopReason: "aborted" },
],
agent: { replaceMessages },
sessionManager: {
fileEntries: [
{ type: "session", id: "session-root" },
{
type: "custom_message",
id: "interrupt",
parentId: "session-root",
customType: "openclaw.sessions_yield_interrupt",
},
{
type: "message",
id: "aborted",
parentId: "interrupt",
message: { role: "assistant", stopReason: "aborted" },
},
],
byId: new Map([
["interrupt", { id: "interrupt" }],
["aborted", { id: "aborted" }],
]),
leafId: "aborted",
_rewriteFile: rewriteFile,
},
};

stripSessionsYieldArtifacts(activeSession as never);

expect(replaceMessages).toHaveBeenCalledWith([
{ role: "user", content: [{ type: "text", text: "hi" }] },
]);
expect(activeSession.sessionManager.fileEntries).toEqual([
{ type: "session", id: "session-root" },
]);
expect(activeSession.sessionManager.byId.has("interrupt")).toBe(false);
expect(activeSession.sessionManager.byId.has("aborted")).toBe(false);
expect(activeSession.sessionManager.leafId).toBe("session-root");
expect(rewriteFile).toHaveBeenCalledTimes(1);
});
});

describe("composeSystemPromptWithHookContext", () => {
it("returns undefined when no hook system context is provided", () => {
expect(composeSystemPromptWithHookContext({ baseSystemPrompt: "base" })).toBeUndefined();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unrelated test additions in a focused compaction PR

The sessions_yield helpers, shouldInjectHeartbeatPrompt, and resolveEmbeddedAgentStreamFn test suites added here are unrelated to the compaction model override fix described in the PR. Bundling unrelated test additions makes the diff harder to review and muddies the git history. Consider splitting them into a separate PR or commit if the team's convention is to keep PRs focused. Not a correctness issue — just a scope observation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.test.ts
Line: 153-250

Comment:
**Unrelated test additions in a focused compaction PR**

The `sessions_yield helpers`, `shouldInjectHeartbeatPrompt`, and `resolveEmbeddedAgentStreamFn` test suites added here are unrelated to the compaction model override fix described in the PR. Bundling unrelated test additions makes the diff harder to review and muddies the git history. Consider splitting them into a separate PR or commit if the team's convention is to keep PRs focused. Not a correctness issue — just a scope observation.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dbe8a026a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const slashIdx = override.indexOf("/");
if (slashIdx > 0) {
const overrideProvider = override.slice(0, slashIdx).trim();
const overrideModel = override.slice(slashIdx + 1).trim() || params.defaultModel;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back to caller model when override suffix is empty

When agents.defaults.compaction.model is set like provider/, this branch resolves overrideModel to params.defaultModel only, so calls that do not supply runtime defaults return model: undefined even if a caller model was provided. buildEmbeddedCompactionRuntimeContext invokes this helper without defaults, so engine-owned compaction can receive a runtime context with no model and fail or skip compaction in that configuration. Use the already-resolved caller model as the fallback (not just defaultModel) to keep behavior consistent across entrypoints.

Useful? React with 👍 / 👎.

@jalehman
jalehman force-pushed the fix/compaction-model-override-56649 branch from 7dbe8a0 to 794d964 Compare March 31, 2026 06:45
@aisle-research-bot

aisle-research-bot Bot commented Mar 31, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Auth profile dropped based on raw provider string, enabling credential fallback/confusion
2 🟠 High Config-controlled compaction provider/model override can exfiltrate transcripts to unintended LLM providers
3 🟡 Medium Sensitive data exposure: full compaction params/config passed to third-party ContextEngine via runtimeContext
1. 🟠 Auth profile dropped based on raw provider string, enabling credential fallback/confusion
Property Value
Severity High
CWE CWE-287
Location src/agents/pi-embedded-runner/compaction-runtime-context.ts:54-62

Description

resolveEmbeddedCompactionTarget() attempts to drop authProfileId only when a config override switches providers, but it compares the override provider against the raw caller-supplied params.provider.

This creates a credential-confusion edge case:

  • If the caller omits provider (or passes whitespace) but does supply an authProfileId intended for the runtime’s default provider, and
  • agents.defaults.compaction.model is configured as "<overrideProvider>/<model>" where overrideProvider equals the effective default provider,

then authProfileId is dropped even though the effective provider did not change. Downstream, getApiKeyForModel({ profileId: authProfileId }) is called with profileId: undefined, causing the auth resolver to fall back to other profiles / env keys / provider keys.

Impact depends on deployment, but can include:

  • Using a more privileged/global API key instead of the intended per-session/per-tenant authProfileId
  • Sending compaction requests under the wrong account (cross-tenant credential mix-up)

Vulnerable code:

const authProfileId =
  overrideProvider !== (params.provider ?? "")?.trim()
    ? undefined
    : (params.authProfileId ?? undefined);

This logic is also used via buildEmbeddedCompactionRuntimeContext(), which currently calls resolveEmbeddedCompactionTarget() without passing defaultProvider/defaultModel, increasing the odds that params.provider is unset while the runtime still has defaults elsewhere.

Recommendation

Compare against the effective provider (after applying defaults), not the raw caller input, and ensure defaults are consistently passed.

For example:

export function resolveEmbeddedCompactionTarget(params: {// ...
  defaultProvider?: string;
  defaultModel?: string;
}) {
  const effectiveProvider = params.provider?.trim() || params.defaultProvider;
  const effectiveModel = params.modelId?.trim() || params.defaultModel;

  const override = params.config?.agents?.defaults?.compaction?.model?.trim();
  if (!override) return { provider: effectiveProvider, model: effectiveModel, authProfileId: params.authProfileId ?? undefined };

  const slashIdx = override.indexOf("/");
  if (slashIdx > 0) {
    const overrideProvider = override.slice(0, slashIdx).trim();
    const overrideModel = override.slice(slashIdx + 1).trim() || params.defaultModel;

    const authProfileId =
      overrideProvider !== (effectiveProvider ?? "")
        ? undefined
        : (params.authProfileId ?? undefined);

    return { provider: overrideProvider, model: overrideModel, authProfileId };
  }

  return { provider: effectiveProvider, model: override, authProfileId: params.authProfileId ?? undefined };
}

Also update buildEmbeddedCompactionRuntimeContext() to pass defaultProvider/defaultModel when available (or ensure callers always pass provider when passing authProfileId).

2. 🟠 Config-controlled compaction provider/model override can exfiltrate transcripts to unintended LLM providers
Property Value
Severity High
CWE CWE-284
Location src/agents/pi-embedded-runner/compaction-runtime-context.ts:34-68

Description

The embedded Pi compaction path allows config.agents.defaults.compaction.model to override both the model and the provider used for compaction. Because compaction processes/transmits conversation history for summarization, an attacker (or untrusted workspace/user config) that can influence this config value can redirect sensitive transcript content to a different third-party provider, bypassing enterprise/provider allowlists, data residency requirements, or other governance controls.

Key points:

  • Input: config.agents.defaults.compaction.model (string) is taken from config and parsed as either provider/model or model.
  • No validation/allowlist is applied to the provider name or model id.
  • Sink: the resolved provider and modelId are used to resolveModelAsync(provider, modelId, ...) and then run compaction (which summarizes transcript/history).

Vulnerable code (provider/model override parsing):

const override = params.config?.agents?.defaults?.compaction?.model?.trim();
...
if (slashIdx > 0) {
  const overrideProvider = override.slice(0, slashIdx).trim();
  const overrideModel = override.slice(slashIdx + 1).trim() || params.defaultModel;
  ...
  return { provider: overrideProvider, model: overrideModel, authProfileId };
}

Vulnerable code (override used to select runtime provider/model for compaction):

const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({ config: params.config, ... });
const provider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER;
const modelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL;
const { model } = await resolveModelAsync(provider, modelId, agentDir, params.config);

Recommendation

Treat agents.defaults.compaction.model as a restricted setting:

  • Enforce an explicit allowlist of permitted compaction providers/models (ideally separate from the primary chat model allowlist), and reject/ignore overrides outside the allowlist.
  • Consider splitting config into provider and model fields with schema-level validation rather than ad-hoc string parsing.
  • If multi-tenant/untrusted workspace config is possible, prevent workspace/project-level settings from changing provider/model used for compaction.

Example (conceptual) validation/allowlist:

const allowed = new Set(["openai/gpt-4o", "anthropic/claude-sonnet-4-5"]);
const requested = `${provider}/${modelId}`;
if (!allowed.has(requested)) {
  throw new Error(`Compaction model not allowed: ${requested}`);
}
3. 🟡 Sensitive data exposure: full compaction params/config passed to third-party ContextEngine via runtimeContext
Property Value
Severity Medium
CWE CWE-201
Location src/agents/pi-embedded-runner/compact.ts:994-1048

Description

compactEmbeddedPiSession() builds a runtimeContext object by spreading all caller-supplied params (including config, workspaceDir, skillsSnapshot, owner identifiers, prompts, etc.) and passes it to the pluggable contextEngine.compact() and runContextEngineMaintenance().

Because context engines can be registered via the public SDK (registerContextEngine() is explicitly for third-party registrations), any non-core or externally supplied ContextEngine implementation will receive this runtime context. This can lead to unintended disclosure of sensitive information such as:

  • internal filesystem paths (workspaceDir, agentDir, sessionFile)
  • identifiers / PII (senderId, ownerNumbers, channel/thread ids)
  • full runtime configuration (config) which may contain secrets or security-relevant settings
  • prompt content (extraSystemPrompt, customInstructions)

Vulnerable code (from changed lines):

const runtimeContext = {
  ...params,
  ...buildEmbeddedCompactionRuntimeContext({// ...
    config: params.config,
    skillsSnapshot: params.skillsSnapshot,
    extraSystemPrompt: params.extraSystemPrompt,
    ownerNumbers: params.ownerNumbers,
  }),
};

const result = await contextEngine.compact({// ...
  runtimeContext,
});

Even if most engines are trusted, this design makes it easy for a third-party engine (or a compromised plugin dependency) to exfiltrate sensitive configuration and user metadata.

Recommendation

Do not pass the full params object to third-party ContextEngines. Instead, whitelist and/or redact fields before handing off to plugin implementations.

Suggested fix (use a minimal allowlisted schema and avoid passing full config):

// Only expose what engines actually need
const runtimeContext = buildEmbeddedCompactionRuntimeContext({
  sessionKey: params.sessionKey,
  messageChannel: params.messageChannel,
  messageProvider: params.messageProvider,
  agentAccountId: params.agentAccountId,
  currentChannelId: params.currentChannelId,
  currentThreadTs: params.currentThreadTs,
  currentMessageId: params.currentMessageId,// consider omitting authProfileId/config/ownerNumbers unless strictly needed
  workspaceDir: resolveUserPath(params.workspaceDir),
  agentDir,
  senderIsOwner: params.senderIsOwner,
  senderId: params.senderId,
  provider: params.provider,
  modelId: params.model,
});

await contextEngine.compact({ ... , runtimeContext });

If engines need configuration, pass a redacted snapshot (e.g., remove secrets/tokens/passwords and any auth-profile material), and document the contract so plugin authors cannot rely on hidden fields from params.


Analyzed PR: #56710 at commit 9984e90

Last updated on: 2026-03-31T17:37:38Z

@jalehman
jalehman force-pushed the fix/compaction-model-override-56649 branch from 794d964 to 79aac03 Compare March 31, 2026 07:21
@jalehman jalehman self-assigned this Mar 31, 2026
@jalehman
jalehman force-pushed the fix/compaction-model-override-56649 branch 4 times, most recently from f80f9dd to c8b3f4c Compare March 31, 2026 22:16
oliviareid-svg and others added 6 commits March 31, 2026 15:24
…ntext engines

The compaction.model config override was only resolved inside
compactEmbeddedPiSessionDirect (compact.ts), which is only reached
by the legacy context engine. Custom context engines with their own
compact() implementation never saw the override, causing overflow and
timeout recovery to use the session's default model instead.

Move the override resolution into buildEmbeddedCompactionRuntimeContext
so the runtime context always carries the correct compaction model
regardless of which context engine handles the compact() call.

Fixes openclaw#56649

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@jalehman
jalehman force-pushed the fix/compaction-model-override-56649 branch from db7f14e to 72550aa Compare March 31, 2026 22:24
@jalehman
jalehman merged commit bf0f33d into openclaw:main Mar 31, 2026
8 checks passed
@jalehman

Copy link
Copy Markdown
Contributor

Merged via squash.

Thanks @oliviareid-svg!

alexanderkreidich pushed a commit to alexanderkreidich/openclaw that referenced this pull request Apr 4, 2026
…ntext engines (openclaw#56710)

Merged via squash.

Prepared head SHA: 72550aa
Co-authored-by: oliviareid-svg <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…ntext engines (openclaw#56710)

Merged via squash.

Prepared head SHA: 72550aa
Co-authored-by: oliviareid-svg <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…ntext engines (openclaw#56710)

Merged via squash.

Prepared head SHA: 72550aa
Co-authored-by: oliviareid-svg <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…ntext engines (openclaw#56710)

Merged via squash.

Prepared head SHA: 72550aa
Co-authored-by: oliviareid-svg <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…ntext engines (openclaw#56710)

Merged via squash.

Prepared head SHA: 72550aa
Co-authored-by: oliviareid-svg <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Nachx639 pushed a commit to Nachx639/clawdbot that referenced this pull request Jun 17, 2026
…ntext engines (openclaw#56710)

Merged via squash.

Prepared head SHA: 72550aa
Co-authored-by: oliviareid-svg <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: openclaw 2026.3.24: compaction.model override ignored in overflow recovery path — causes cascade of HTTP 400 failures

2 participants