Skip to content

fix(memory-wiki): skip bridge pruning when public artifacts list is empty#71764

Merged
fabianwilliams merged 1 commit into
openclaw:mainfrom
mara-mindless:fix/memory-wiki-bridge-prune-race-condition
Apr 25, 2026
Merged

fix(memory-wiki): skip bridge pruning when public artifacts list is empty#71764
fabianwilliams merged 1 commit into
openclaw:mainfrom
mara-mindless:fix/memory-wiki-bridge-prune-race-condition

Conversation

@mara-mindless

Copy link
Copy Markdown
Contributor

Problem

When memory-core plugin is not loaded (e.g. CLI context), listActiveMemoryPublicArtifacts returns an empty array. The syncMemoryWikiBridgeSources function then calls pruneImportedSourceEntries with an empty activeKeys Set, which removes ALL bridge-imported entries.

This causes a race condition: any CLI wiki command after a gateway bridge import destroys all imported artifacts.

Fix

Skip pruning when publicArtifacts is empty, since that means memory-core is not loaded and we cannot know the actual artifact list.

Fixes #68373

@mara-mindless
mara-mindless requested review from a team as code owners April 25, 2026 20:54
@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation app: web-ui App: web-ui gateway Gateway runtime extensions: memory-core Extension: memory-core scripts Repository scripts agents Agent runtime and tooling extensions: qa-lab extensions: memory-wiki size: XL labels Apr 25, 2026
@greptile-apps

greptile-apps Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a destructive pruning race condition in syncMemoryWikiBridgeSources: when memory-core is not loaded (CLI context), listActiveMemoryPublicArtifacts returns [], which previously caused pruneImportedSourceEntries to delete all bridge entries. It also adds a cron-service retry path in dreaming.ts for a startup race, and extracts a resolveSafeTimeoutDelayMs utility to prevent 32-bit timer overflow.

  • The publicArtifacts.length > 0 guard in bridge.ts conflates two distinct states: "memory-core not loaded" and "memory-core loaded but all artifacts deleted." In the latter case pruning should proceed, but the guard skips it, leaving stale wiki entries indefinitely.

Confidence Score: 3/5

Safe to merge with caution — the guard prevents the reported data-loss bug, but introduces a secondary correctness gap when memory-core is loaded with zero artifacts.

One P1 finding: the publicArtifacts.length > 0 heuristic is not a reliable discriminator for "plugin not registered," meaning stale bridge entries persist whenever a user removes all artifacts while memory-core remains loaded. This is a real present defect on a changed code path, warranting a score below the P1 ceiling.

extensions/memory-wiki/src/bridge.ts — the shouldPrune guard needs a proper plugin-registration check rather than an artifact-count proxy.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/memory-wiki/src/bridge.ts
Line: 254-262

Comment:
**Guard conflates "plugin not loaded" with "plugin loaded, no artifacts"**

`listActiveMemoryPublicArtifacts` returns `[]` both when `memoryPluginState.capability` is `undefined` (memory-core not registered) **and** when memory-core is registered but `listArtifacts` returns an empty list (user deleted all artifacts). The `publicArtifacts.length > 0` guard therefore skips pruning in both cases, leaving stale bridge entries in the wiki indefinitely whenever all artifacts are genuinely removed while memory-core is still loaded.

A safer discriminator would check whether the capability is registered at all, rather than relying on the artifact count as a proxy. For example, exporting `isMemoryCapabilityRegistered()` from `memory-state.ts` and threading it through the plugin-sdk interface would let the guard be `const shouldPrune = isMemoryCapabilityRegistered()` instead of `publicArtifacts.length > 0`.

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.ts
Line: 724-727

Comment:
**Closure captures a local variable, not the resolved value**

`resolveStartupCron = () => cron` closes over the `let cron` local variable from this invocation of `reconcileManagedDreamingCron`. In JavaScript, closures capture the variable binding, so if `cron` were reassigned after this line within the same call, `resolveStartupCron()` would return the new value. While `cron` is not reassigned here after this point, an explicit capture is safer and makes the intent clearer:

```suggestion
          resolveStartupCron = ((): (() => CronServiceLike | null) => {
            const captured = cron;
            return () => captured;
          })();
```

This eliminates any ambiguity about whether the cached reference is the `cron` value at assignment time.

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

Reviews (1): Last reviewed commit: "fix(memory-wiki): skip bridge pruning wh..." | Re-trigger Greptile

Comment thread extensions/memory-wiki/src/bridge.ts Outdated
Comment on lines +254 to +262
const shouldPrune = publicArtifacts.length > 0;
const removedCount = shouldPrune
? await pruneImportedSourceEntries({
vaultRoot: params.config.vault.path,
group: "bridge",
activeKeys,
state,
})
: 0;

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 Guard conflates "plugin not loaded" with "plugin loaded, no artifacts"

listActiveMemoryPublicArtifacts returns [] both when memoryPluginState.capability is undefined (memory-core not registered) and when memory-core is registered but listArtifacts returns an empty list (user deleted all artifacts). The publicArtifacts.length > 0 guard therefore skips pruning in both cases, leaving stale bridge entries in the wiki indefinitely whenever all artifacts are genuinely removed while memory-core is still loaded.

A safer discriminator would check whether the capability is registered at all, rather than relying on the artifact count as a proxy. For example, exporting isMemoryCapabilityRegistered() from memory-state.ts and threading it through the plugin-sdk interface would let the guard be const shouldPrune = isMemoryCapabilityRegistered() instead of publicArtifacts.length > 0.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/memory-wiki/src/bridge.ts
Line: 254-262

Comment:
**Guard conflates "plugin not loaded" with "plugin loaded, no artifacts"**

`listActiveMemoryPublicArtifacts` returns `[]` both when `memoryPluginState.capability` is `undefined` (memory-core not registered) **and** when memory-core is registered but `listArtifacts` returns an empty list (user deleted all artifacts). The `publicArtifacts.length > 0` guard therefore skips pruning in both cases, leaving stale bridge entries in the wiki indefinitely whenever all artifacts are genuinely removed while memory-core is still loaded.

A safer discriminator would check whether the capability is registered at all, rather than relying on the artifact count as a proxy. For example, exporting `isMemoryCapabilityRegistered()` from `memory-state.ts` and threading it through the plugin-sdk interface would let the guard be `const shouldPrune = isMemoryCapabilityRegistered()` instead of `publicArtifacts.length > 0`.

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

Comment thread extensions/memory-core/src/dreaming.ts Outdated
Comment on lines +724 to +727
if (cron) {
// Refresh the startup capture so subsequent calls resolve immediately.
resolveStartupCron = () => cron;
}

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 Closure captures a local variable, not the resolved value

resolveStartupCron = () => cron closes over the let cron local variable from this invocation of reconcileManagedDreamingCron. In JavaScript, closures capture the variable binding, so if cron were reassigned after this line within the same call, resolveStartupCron() would return the new value. While cron is not reassigned here after this point, an explicit capture is safer and makes the intent clearer:

Suggested change
if (cron) {
// Refresh the startup capture so subsequent calls resolve immediately.
resolveStartupCron = () => cron;
}
resolveStartupCron = ((): (() => CronServiceLike | null) => {
const captured = cron;
return () => captured;
})();

This eliminates any ambiguity about whether the cached reference is the cron value at assignment time.

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

Comment:
**Closure captures a local variable, not the resolved value**

`resolveStartupCron = () => cron` closes over the `let cron` local variable from this invocation of `reconcileManagedDreamingCron`. In JavaScript, closures capture the variable binding, so if `cron` were reassigned after this line within the same call, `resolveStartupCron()` would return the new value. While `cron` is not reassigned here after this point, an explicit capture is safer and makes the intent clearer:

```suggestion
          resolveStartupCron = ((): (() => CronServiceLike | null) => {
            const captured = cron;
            return () => captured;
          })();
```

This eliminates any ambiguity about whether the cached reference is the `cron` value at assignment time.

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!

@mara-mindless
mara-mindless force-pushed the fix/memory-wiki-bridge-prune-race-condition branch from 224036f to cbc9c2d Compare April 25, 2026 20:57
@openclaw-barnacle openclaw-barnacle Bot added size: XS and removed docs Improvements or additions to documentation app: web-ui App: web-ui gateway Gateway runtime extensions: memory-core Extension: memory-core scripts Repository scripts agents Agent runtime and tooling extensions: qa-lab size: XL labels Apr 25, 2026
@mara-mindless
mara-mindless force-pushed the fix/memory-wiki-bridge-prune-race-condition branch from cbc9c2d to f473616 Compare April 25, 2026 21:03
When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
@mara-mindless
mara-mindless force-pushed the fix/memory-wiki-bridge-prune-race-condition branch from f473616 to 3d3b195 Compare April 25, 2026 21:08

@fabianwilliams fabianwilliams 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.

Targeted fix — guarding the prune step on getMemoryCapabilityRegistration prevents a CLI-context invocation from interpreting an empty publicArtifacts list as 'all bridges should be removed.' Comment + #68373 link make the trap clear. Export added cleanly via memory-host-sdk runtime-core. LGTM.

@fabianwilliams
fabianwilliams merged commit 7f57895 into openclaw:main Apr 25, 2026
60 of 64 checks passed
@mara-mindless
mara-mindless deleted the fix/memory-wiki-bridge-prune-race-condition branch April 25, 2026 21:25
ayesha-aziz123 pushed a commit to ayesha-aziz123/openclaw that referenced this pull request Apr 26, 2026
…penclaw#71764)

When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…penclaw#71764)

When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penclaw#71764)

When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
…penclaw#71764)

When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…penclaw#71764)

When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
…penclaw#71764)

When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…penclaw#71764)

When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes openclaw#68373
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Regression v2026.4.15] Cached plugin restore drops memory capability, breaks wiki bridge imports

2 participants