Raise eligible Linux child processes own oom_score_adj from a child-side /bin/sh exec shim so cgroup memory pressure prefers transient workers over the long-lived gateway. Cover supervisor children, PTY shells, MCP stdio servers, and OpenClaw-launched browser processes through the shared process runtime seam.
Harden the wrapper for distroless images, shell startup env, per-child and process-level opt-outs, dash-compatible exec, and leading-dash command names. Document Linux verification and OOM behavior.
Fixes#70404.
Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
* fix(pair): render pair qr as media
* fix(gateway): preserve media reply threading
* fix(gateway): harden webchat media replies
* fix(plugin-sdk): keep trustedLocalMedia internal
* docs(changelog): note pair qr media fix
* Update CHANGELOG with recent fixes and enhancements
Updated changelog to include recent fixes and enhancements.
* fix(memory/dreaming): surface blocked status in memory status when heartbeat disabled for main
Replace the hand-rolled heartbeat-rules logic in resolveDreamingBlockedReason
with the shared resolveHeartbeatSummaryForAgent helper, promoted from core to
the plugin-sdk via infra-runtime. Collapses the two disabled-reason branches
into a single message that points at a new Troubleshooting section in the
dreaming docs, so the silent-failure mode described in openclaw/openclaw#69843
becomes legible without the extension re-encoding heartbeat-enablement rules.
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* refactor(memory/dreaming): share resolveDreamingBlockedReason across cli and /dreaming surfaces
- Move resolveDreamingBlockedReason from cli.runtime.ts into dreaming.ts as an exported helper and pin its heartbeat check to DEFAULT_AGENT_ID (now exported from plugin-sdk/routing) so the status-line check agrees with the cron's hardcoded sessionTarget even when the configured default agent is not main.
- Render the blocked reason from formatStatus in dreaming-command.ts directly under the enabled line, so /dreaming status, /dreaming on, /dreaming off, and bare /dreaming all flag that the cron is blocked instead of implying dreaming is healthy.
- Tighten the blocked-reason text to lead with user impact ('dreaming is enabled but will not run because heartbeat is disabled for main'), so operators immediately understand the config is toggled on but nothing is actually running.
- Tighten the dreaming Troubleshooting copy to name main explicitly and mention both surfaces.
- Add tests locking the new behavior across cli.test.ts (default-agent=ops still reports blocked for main) and dreaming-command.test.ts (/dreaming status ordering, /dreaming on surfacing, healthy-heartbeat omission).
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* fix(memory/dreaming): check heartbeat for the resolved default agent, not the literal 'main'
sessionTarget: 'main' is a cron session-type enum variant meaning 'the default agent's main session', not an agent id (see src/cron/service/jobs.ts). buildManagedDreamingCronJob does not set agentId, and cron runtime resolves the missing agentId through resolveDefaultAgentId(cfg) before enqueuing or waking. The previous pin to DEFAULT_AGENT_ID could produce a false 'blocked' reading when a configured default agent is not 'main' and its heartbeat is fine, and could miss a real block when the default agent is not 'main' and that agent's heartbeat is actually off.
Switch resolveDreamingBlockedReason to resolveDefaultAgentId(cfg) and interpolate the resolved agent id into the message so the blocked line names the agent whose heartbeat is the blocker. Introduce a narrow local CRON_SESSION_TARGET_MAIN constant for the cron session-type enum variant (used by the sessionTarget type and value) so the remaining 'main' literal is semantically distinct from any agent id. Revert the DEFAULT_AGENT_ID export addition on plugin-sdk/routing; memory-core no longer needs it. Update the Troubleshooting doc wording and the cli test that was locking the wrong behaviour.
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* fix(memory/dreaming): align blocked check with server-cron wake's defaults-only heartbeat
resolveDreamingBlockedReason was using resolveHeartbeatSummaryForAgent, which merges agents.defaults.heartbeat with agents.list[].heartbeat. The managed dreaming cron leaves job.agentId and job.sessionKey unset, so server-cron's wake wrapper cannot look up a per-agent entry and calls runHeartbeatOnce with agents.defaults.heartbeat only. Using the summary helper would disagree with the actual wake when the default agent overrides heartbeat.every differently from the defaults (either direction — false blocked when the override would run, or false healthy when defaults block).
Mirror the wake path explicitly: rule-1 enablement via isHeartbeatEnabledForAgent against the default agent, rule-3 interval via resolveHeartbeatIntervalMs with defaults-only heartbeat config. Comment points at server-cron so a future cleanup of that latent override-propagation gap sees the coupling.
Refs openclaw/openclaw#69843.
* perf(plugin-sdk): per-phase + per-jiti-call probes for bundled channel entries
Extends the existing OPENCLAW_PLUGIN_LOAD_PROFILE infrastructure (see
src/plugins/loader.ts `profilePluginLoaderSync` and src/plugins/source-loader.ts)
with two new probe sites inside src/plugin-sdk/channel-entry-contract.ts:
1. `bundled-register:<phase>` — wraps each phase of `defineBundledChannelEntry`'s
register() callback (`setChannelRuntime`, `loadChannelPlugin`, `registerChannel`,
`registerCliMetadata`, `registerFull`). Lets us pinpoint which phase of plugin
registration is responsible for cold-start cost on a per-plugin basis.
2. `bundled-entry-module-load` — instruments `loadBundledEntryModuleSync` and
reports `getJitiMs` (jiti loader factory) vs `jitiCallMs` (actual graph walk
+ transpile + ESM linking) separately. Lets us distinguish alias-map / loader
setup overhead from import-graph traversal cost on a per-module basis.
Both probes are gated on OPENCLAW_PLUGIN_LOAD_PROFILE=1 and have zero overhead
when the env flag is unset (early return before any `performance.now()` call).
Log format matches the existing `[plugin-load-profile]` line shape so existing
log scrapers continue to work.
The helper is a file-local mirror of `profilePluginLoaderSync` rather than a
new SDK export — keeps the SDK boundary narrow per src/plugin-sdk/AGENTS.md
and avoids cross-importing host internals.
Used to validate PR #69317 (slack startup perf) — measurements showed slack
`setChannelRuntime` dropping from 13183ms to 67ms after barrel narrowing,
which would have been undiagnosable without these per-phase probes.
* perf(plugins): per-plugin register() probe in plugin loader
Adds a `phase=${registrationMode}:register` probe wrapping each call to
`runPluginRegisterSync(register, api)` in src/plugins/loader.ts. Emits the
established `[plugin-load-profile]` line shape via `profilePluginLoaderSync`,
gated on OPENCLAW_PLUGIN_LOAD_PROFILE=1.
Two call sites are wrapped:
- The main load path (registrationMode is dynamic: "snapshot", "validate",
"full") at the post-snapshot register block. Emits e.g.
`phase=full:register plugin=slack elapsedMs=14102.1 source=...`
- The cli-metadata-only path (registrationMode hardcoded to "cli-metadata")
for fast `--metadata` boot flows.
Together with the existing `phase=full` (entire load) and `phase=source-loader`
probes plus the `bundled-register:*` and `bundled-entry-module-load` probes
added in the previous commit, this gives a full breakdown:
- `phase=full plugin=slack` — total cost from import through register return
- `phase=full:register plugin=slack` — just the register() callback (NEW)
- `phase=bundled-register:setChannelRuntime plugin=slack` — sub-phase
- `phase=bundled-register:loadChannelPlugin plugin=slack` — sub-phase
- `phase=bundled-entry-module-load plugin=(bundled-entry)` — per-module load
Lets you `sort -k4 -n -r` the log output to find the slowest plugin's
register() call across all bundled+third-party plugins, then drill in via
the sub-phase probes for bundled entries.
* perf(plugins): consolidate plugin-load-profile primitives in shared module
Extracts the previously duplicated `shouldProfilePluginLoader` /
`profilePluginLoaderSync` helpers into a new `src/plugins/plugin-load-profile.ts`
module. Removes 3 file-local copies of the same env-flag check and 2
near-duplicate `try { run() } finally { console.error(...) }` wrappers.
Files updated:
- NEW src/plugins/plugin-load-profile.ts — sole owner of:
shouldProfilePluginLoader()
profilePluginLoaderSync<T>({phase, pluginId?, source, run, extras?})
formatPluginLoadProfileLine({phase, pluginId?, source, elapsedMs, extras?})
- src/plugins/loader.ts — drop file-local copies, import shared helper
(existing 4 + new 2 call sites unchanged in shape)
- src/plugins/source-loader.ts — drop renamed local copy
(`shouldProfilePluginSourceLoader`), use shared helper with
`pluginId: "(direct)"` to preserve the existing `plugin=(direct)` field
- src/plugin-sdk/channel-entry-contract.ts — drop file-local copies and
inline `profileStep` closure; use shared `profilePluginLoaderSync` directly
at all 5 `bundled-register:*` call sites; dual-timing
`bundled-entry-module-load` probe uses `formatPluginLoadProfileLine` with
ordered `extras` for `getJitiMs`/`jitiCallMs`
Log line format is byte-for-byte identical to before (validated against
3 cases: standard, with pluginId, dual-timing). The `extras` API is
intentionally an ordered tuple list (not a record) so that scrapers see
deterministic field order between `elapsedMs=` and `source=`.
Net: +155/-87 lines across 4 files, removing ~60 lines of duplication
while exposing a stable, documented probe surface.
Verified:
- pnpm tsgo (core) — 0 errors
- pnpm lint on all 4 files — 0 warnings, 0 errors
- pnpm test src/plugins/loader.test.ts — 102/102
- pnpm test src/plugins/contracts/plugin-entry-guardrails.test.ts — 7/7
- pnpm test src/plugin-sdk/channel-entry-contract.test.ts — 4/4
- Standalone formatter smoke test — output matches existing format byte-for-byte
* refactor(plugins): rename profilePluginLoaderSync to withProfile and bind scope at register sites
* fix(plugin-sdk): zero jiti sub-step timings on Win32 nodeRequire fast-path