Skip to content

fix: reduce log level for narrative session cleanup scope failures#68130

Closed
Linux2010 wants to merge 4 commits into
openclaw:mainfrom
Linux2010:fix/issue-68074
Closed

fix: reduce log level for narrative session cleanup scope failures#68130
Linux2010 wants to merge 4 commits into
openclaw:mainfrom
Linux2010:fix/issue-68074

Conversation

@Linux2010

Copy link
Copy Markdown
Contributor

Summary

The memory-core dreaming feature was generating spurious warning logs every time it ran, stating "missing scope: operator.admin". This occurred because narrative session cleanup calls subagent.deleteSession() which requires operator.admin scope, but the dreaming cron runs in the main session without this scope.

Changes

  • Modified error handling in dreaming-narrative.ts to check for expected scope failures
  • Log at debug level for missing operator.admin scope (expected, best-effort cleanup)
  • Preserve warn level for genuine errors

Testing

  • Narrative cleanup now silently skips when scope is missing
  • Actual errors are still surfaced appropriately

Fixes #68074

Developer added 4 commits April 17, 2026 03:54
Fixes openclaw#67949

OpenClaw was sending a 'think' parameter to Ollama for all models when
thinkingLevel was set, but some smaller models like qwen2.5:0.5b do not
support thinking mode, causing a 400 error: "does not support thinking".

The fix uses isReasoningModelHeuristic() to detect if a model supports
thinking (based on model ID patterns like 'r1', 'reasoning', 'think',
'reason') and only sends the think parameter for models that are known
to support it.

Changes:
- Import isReasoningModelHeuristic from provider-models.js
- Check supportsThinking before applying the think wrapper
- Update tests to use reasoning model IDs (deepseek-r1:8b)
- Add new tests for non-reasoning models (qwen2.5:0.5b)
…y resolution

The PROVIDER_ENV_API_KEY_CANDIDATES export was calling
resolveProviderEnvApiKeyCandidates() at module load time,
which triggered loadPluginManifestRegistry() before bundled
plugins were fully available. This caused OpenRouter and other
bundled provider auth env vars to be missing from the candidate
map at startup.

Fixes openclaw#67989: OpenRouter provider not routing requests to models

The fix delegates to the existing lazy PROVIDER_AUTH_ENV_VAR_CANDIDATES
proxy from provider-env-vars.ts, which defers manifest registry
loading until first property access. This ensures bundled plugin
metadata (including providerAuthEnvVars from manifests like
OpenRouter's) is available when needed.

Tests added to verify:
- openrouter env var resolution works
- lazy loading behavior is preserved
Addresses review feedback from Codex/Greptile/Aisle on PR openclaw#67958
…out operator.admin scope

When the dreaming cron runs in the main session, it lacks operator.admin
scope required by subagent.deleteSession(). This is expected behavior,
not an error condition.

Changed the cleanup failure log from warn to debug level when the error
message contains 'missing scope: operator.admin', making this best-effort
cleanup less noisy in logs while still surfacing genuine errors at warn.

Fixes openclaw#68074
@Linux2010
Linux2010 requested a review from a team as a code owner April 17, 2026 13:46
@aisle-research-bot

aisle-research-bot Bot commented Apr 17, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

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

# Severity Title
1 🟡 Medium Prototype pollution via untrusted plugin providerAuthEnvVars keys in provider env-var candidate resolution
1. 🟡 Prototype pollution via untrusted plugin `providerAuthEnvVars` keys in provider env-var candidate resolution
Property Value
Severity Medium
CWE CWE-1321
Location src/secrets/provider-env-vars.ts:114-134

Description

resolveManifestProviderAuthEnvVarCandidates() builds a plain object map ({}) of provider IDs to env-var names using keys from plugin manifest metadata (plugin.providerAuthEnvVars).

  • providerId is taken from Object.entries(plugin.providerAuthEnvVars) (plugin-controlled, including workspace plugins by default)
  • appendUniqueEnvVarCandidates() writes into a normal object with target[normalizedProviderId] without guarding against special keys
  • A manifest can supply keys such as __proto__, constructor, or prototype, which in JavaScript can mutate the target object's prototype (or otherwise create confusing inherited lookups)
  • The resulting polluted object is then used for property access (e.g., candidates[target] and later via the lazy proxy getResolved()[prop]), enabling unexpected env-var candidate resolution and potentially impacting security decisions around which secrets are considered for providers

Vulnerable code:

const candidates: Record<string, string[]> = {};
...
for (const [providerId, keys] of Object.entries(plugin.providerAuthEnvVars) ...) {
  appendUniqueEnvVarCandidates(candidates, providerId, keys);
}
...
const bucket = (target[normalizedProviderId] ??= []);

Recommendation

Harden map construction against prototype pollution:

  1. Use a null-prototype object for dictionaries that accept untrusted keys.
  2. Explicitly reject dangerous keys (__proto__, prototype, constructor).
  3. When reading from dictionaries, use Object.hasOwn() checks instead of direct property access where appropriate.

Secure example:

const candidates: Record<string, string[]> = Object.create(null);

function isSafeKey(k: string): boolean {
  return k !== "__proto__" && k !== "prototype" && k !== "constructor";
}

function appendUniqueEnvVarCandidates(
  target: Record<string, string[]>,
  providerId: string,
  keys: readonly string[],
) {
  const normalizedProviderId = providerId.trim();
  if (!normalizedProviderId || !isSafeKey(normalizedProviderId) || keys.length === 0) return;
  const bucket = (target[normalizedProviderId] ??= []);
  ...
}

Analyzed PR: #68130 at commit 3cae1ff

Last updated on: 2026-04-17T13:48:00Z

@openclaw-barnacle openclaw-barnacle Bot added extensions: memory-core Extension: memory-core agents Agent runtime and tooling size: M labels Apr 17, 2026
@greptile-apps

greptile-apps Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bundles three independent fixes: (1) reduces the dreaming narrative cleanup log from warn to debug when deleteSession fails due to missing operator.admin scope (expected for the cron context); (2) guards the Ollama think parameter injection behind isReasoningModelHeuristic so non-reasoning models like qwen2.5 don't receive a param that causes a 400 error, expanding the heuristic regex to cover qwen3 and qwq; and (3) changes PROVIDER_ENV_API_KEY_CANDIDATES in model-auth-env-vars.ts to delegate to the existing lazy Proxy rather than eagerly resolving at import time.

  • The lazy-loading test in model-auth-env-vars.test.ts has a misplaced assertion: expect(loadPluginManifestRegistry).not.toHaveBeenCalled() runs synchronously before the dynamic import() resolves, so it cannot catch an eager-loading regression. The assertion should be moved to after await modPromise.
  • The scope-error check in dreaming-narrative.ts uses errMsg.includes("missing scope: operator.admin") — consider preferring a typed error field if one is available from the auth layer.

Confidence Score: 5/5

Safe to merge; all findings are non-blocking style/quality suggestions.

Both inline findings are P2: the lazy-loading test has a misplaced assertion (doesn't block functionality, only weakens the regression guard), and the string-based scope check is fragile but the worst-case degradation is higher log verbosity. The implementation changes themselves are correct.

src/agents/model-auth-env-vars.test.ts — the "is lazy-loaded" test assertion needs to be repositioned after await modPromise to actually validate the lazy-loading property.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/model-auth-env-vars.test.ts
Line: 42

Comment:
**Lazy-load assertion runs before module initializes**

`import()` always resolves asynchronously (Promise microtask), so the module's top-level code hasn't executed yet when this synchronous assertion runs. This means the test passes even if `model-auth-env-vars.ts` eagerly called `loadPluginManifestRegistry` at import time — it wouldn't be caught because the module hasn't been evaluated by the time this check executes.

Move the "not called" assertion to after the `await modPromise` to actually verify the lazy-loading property:

```suggestion
    const mod = await modPromise;

    // Module is now fully initialized; registry should NOT have been called yet
    expect(loadPluginManifestRegistry).not.toHaveBeenCalled();

    // Now access the lazy export
```

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

---

This is a comment left during a code review.
Path: extensions/memory-core/src/dreaming-narrative.ts
Line: 938

Comment:
**Fragile string-based scope detection**

Matching on `errMsg.includes("missing scope: operator.admin")` couples the suppression logic to the exact error message format. If the auth layer ever changes that string (e.g. to `"scope missing: operator.admin"` or adds localization), this silently degrades back to the warn-level noise the PR is trying to eliminate.

If the error type that `deleteSession` throws has a structured code or scope field (e.g. `err.code`, `err.scope`, or an `isScopeError()` helper), prefer checking that instead. If no typed path exists today, a narrow inline comment explaining the message format's stability helps future readers understand the coupling.

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

Reviews (1): Last reviewed commit: "fix(memory-core): reduce log noise for n..." | Re-trigger Greptile

const modPromise = import("./model-auth-env-vars.js");

// The manifest registry should NOT have been called yet
expect(loadPluginManifestRegistry).not.toHaveBeenCalled();

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 Lazy-load assertion runs before module initializes

import() always resolves asynchronously (Promise microtask), so the module's top-level code hasn't executed yet when this synchronous assertion runs. This means the test passes even if model-auth-env-vars.ts eagerly called loadPluginManifestRegistry at import time — it wouldn't be caught because the module hasn't been evaluated by the time this check executes.

Move the "not called" assertion to after the await modPromise to actually verify the lazy-loading property:

Suggested change
expect(loadPluginManifestRegistry).not.toHaveBeenCalled();
const mod = await modPromise;
// Module is now fully initialized; registry should NOT have been called yet
expect(loadPluginManifestRegistry).not.toHaveBeenCalled();
// Now access the lazy export
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/model-auth-env-vars.test.ts
Line: 42

Comment:
**Lazy-load assertion runs before module initializes**

`import()` always resolves asynchronously (Promise microtask), so the module's top-level code hasn't executed yet when this synchronous assertion runs. This means the test passes even if `model-auth-env-vars.ts` eagerly called `loadPluginManifestRegistry` at import time — it wouldn't be caught because the module hasn't been evaluated by the time this check executes.

Move the "not called" assertion to after the `await modPromise` to actually verify the lazy-loading property:

```suggestion
    const mod = await modPromise;

    // Module is now fully initialized; registry should NOT have been called yet
    expect(loadPluginManifestRegistry).not.toHaveBeenCalled();

    // Now access the lazy export
```

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

// Best-effort cleanup: missing operator.admin scope is expected when running
// in main session (dreaming cron), so log at debug level instead of warn.
const errMsg = formatErrorMessage(cleanupErr);
if (errMsg.includes("missing scope: operator.admin")) {

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 Fragile string-based scope detection

Matching on errMsg.includes("missing scope: operator.admin") couples the suppression logic to the exact error message format. If the auth layer ever changes that string (e.g. to "scope missing: operator.admin" or adds localization), this silently degrades back to the warn-level noise the PR is trying to eliminate.

If the error type that deleteSession throws has a structured code or scope field (e.g. err.code, err.scope, or an isScopeError() helper), prefer checking that instead. If no typed path exists today, a narrow inline comment explaining the message format's stability helps future readers understand the coupling.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/memory-core/src/dreaming-narrative.ts
Line: 938

Comment:
**Fragile string-based scope detection**

Matching on `errMsg.includes("missing scope: operator.admin")` couples the suppression logic to the exact error message format. If the auth layer ever changes that string (e.g. to `"scope missing: operator.admin"` or adds localization), this silently degrades back to the warn-level noise the PR is trying to eliminate.

If the error type that `deleteSession` throws has a structured code or scope field (e.g. `err.code`, `err.scope`, or an `isScopeError()` helper), prefer checking that instead. If no typed path exists today, a narrow inline comment explaining the message format's stability helps future readers understand the coupling.

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

@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: 3cae1ff2fa

ℹ️ 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".

Comment on lines +939 to +940
params.logger.debug(
`memory-core: narrative session cleanup skipped (no operator.admin scope) for ${params.data.phase} phase`,

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.

P1 Badge Guard debug logging in cleanup scope-error branch

The new missing-scope branch calls params.logger.debug(...), but this function’s Logger contract only requires info/warn/error. In environments that pass a minimal logger (which already matches this file’s declared type), a missing scope: operator.admin cleanup error will now throw TypeError: ...debug is not a function inside finally, turning best-effort cleanup into a hard failure and skipping the remaining scrub path.

Useful? React with 👍 / 👎.

@prtags

prtags Bot commented Apr 23, 2026

Copy link
Copy Markdown

Related work from PRtags group ultimate-chow-4ei0

Title: Open PR duplicate: [Bug]: memory-core: narrative session cleanup fails with "missing scope: operator.admin"

Number Title
#68020 fix(memory-core): silence expected operator.admin scope miss in dreaming cleanup
#68087 fix(memory-core): downgrade cleanup warning to debug when missing operator.admin scope
#68130* fix: reduce log level for narrative session cleanup scope failures
#68312 fix(memory-core): downgrade narrative cleanup WARN to debug for missing-scope errors
#68681 fix(memory-core): suppress expected dreaming cleanup scope warnings

* This PR

@clawsweeper

clawsweeper Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Closing this as duplicate or superseded after Codex automated review.

Close #68130 as duplicate/superseded. It targets the same memory-core missing scope: operator.admin narrative cleanup warning already grouped with #68020, #68087, #68312, and #68681; #68020 is the canonical maintainer PR for the remaining focused cleanup policy. #68130 is also not a clean landing vehicle because its memory-core patch calls logger.debug outside the current local logger contract and the branch bundles unrelated Ollama/model-auth changes.

Best possible solution:

Close #68130 as a duplicate/superseded contributor PR. Keep the remaining cleanup-warning policy on the canonical maintainer item #68020 or a new focused PR that preserves the local logger contract, keeps genuine cleanup failures visible, and splits unrelated Ollama/model-auth fixes into separate reviewed PRs.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against d54d2d6b9b8a.

@clawsweeper clawsweeper Bot closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling extensions: memory-core Extension: memory-core size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: memory-core: narrative session cleanup fails with "missing scope: operator.admin"

1 participant