fix(memory/dreaming): surface blocked status when heartbeat is disabled for main#69875
Conversation
…artbeat 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, openclaw#46046.
Greptile SummaryThis PR surfaces a Confidence Score: 5/5Safe to merge — observability-only change with correct logic, clean tests, and no semantic side effects. The two blocking conditions ( No files require special attention. Reviews (1): Last reviewed commit: "fix(memory/dreaming): surface blocked st..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a5c0cf39c
ℹ️ 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".
…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#69843, openclaw#46046.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a202090bb9
ℹ️ 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".
… 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#69843, openclaw#46046.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c84f082b8
ℹ️ 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".
…aults-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#69843.
…s disabled for main (openclaw#69875)" This reverts commit 529577e. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land.
Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (#69875)" This reverts commit 529577e. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR #70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR #70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR #70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR #70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR #70737 review (chatgpt-codex-connector, P1): the revert of PR #69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR #70737. --------- Co-authored-by: Josh Lehman <[email protected]>
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (openclaw#69875)" This reverts commit 529577e. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR openclaw#70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR openclaw#70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR openclaw#70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR openclaw#70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR openclaw#70737. --------- Co-authored-by: Josh Lehman <[email protected]>
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (openclaw#69875)" This reverts commit 6ecf145. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR openclaw#70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR openclaw#70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR openclaw#70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR openclaw#70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR openclaw#70737. --------- Co-authored-by: Josh Lehman <[email protected]>
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (openclaw#69875)" This reverts commit 3c75d5b. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR openclaw#70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR openclaw#70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR openclaw#70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR openclaw#70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR openclaw#70737. --------- Co-authored-by: Josh Lehman <[email protected]>
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (openclaw#69875)" This reverts commit 76b2f6d. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR openclaw#70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR openclaw#70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR openclaw#70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR openclaw#70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR openclaw#70737. --------- Co-authored-by: Josh Lehman <[email protected]>
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (openclaw#69875)" This reverts commit 6f8bb8f. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR openclaw#70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR openclaw#70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR openclaw#70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR openclaw#70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR openclaw#70737. --------- Co-authored-by: Josh Lehman <[email protected]>
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (openclaw#69875)" This reverts commit f8e5e50. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR openclaw#70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR openclaw#70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR openclaw#70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR openclaw#70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR openclaw#70737. --------- Co-authored-by: Josh Lehman <[email protected]>
…ed for main (openclaw#69875) * 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#69843 becomes legible without the extension re-encoding heartbeat-enablement rules. Refs openclaw#69843, 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#69843, 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#69843, 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#69843.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (openclaw#69875)" This reverts commit 7601897. Making way for the dreaming-vs-heartbeat decoupling from Josh's josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming cron to isolated agent turns (sessionTarget: "isolated") so dreaming no longer requires heartbeat to fire. Once the cron no longer rides the heartbeat path, the blocked-reason observability has nothing left to report — removing it cleanly here before the cherry-picks land. * openclaw-3ba.1: move managed dreaming cron to isolated agent turns * openclaw-46d: claim cron runs before embedded attempts * openclaw-575: disable managed dreaming cron delivery * openclaw-575: accept wrapped dreaming cron tokens * openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus * openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus * openclaw-cd9: suppress role-label reflection tags in rem dreaming * openclaw-b49: stop narrative timeouts from blocking dreaming cron * openclaw-b49: keep managed dreaming cron out of diary subagents * openclaw-ff9: restore cron dream diary generation without serial waits * openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes * openclaw-ff9: detach cron dream diary generation from run completion * openclaw-ff9: defer cron diary task startup until after cron completion * doctor/cron: migrate stale managed dreaming jobs to isolated agent turns After the dreaming cron moved off the heartbeat path to sessionTarget: "isolated" + payload.kind: "agentTurn" (see the preceding memory-core changes), users with existing ~/.openclaw/cron/jobs.json entries in the old sessionTarget: "main" + payload.kind: "systemEvent" shape still carry stale jobs until the gateway restart reconcile rewrites them. Add a dreaming-specific cron migration to the existing maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and "openclaw doctor --fix") rewrites those jobs without needing a gateway restart. Match lives in a new doctor-cron-dreaming-payload-migration helper alongside the existing legacy-delivery and store-migration files. The matching uses the memory-core managed-job name and description tag plus the short-term-promotion payload token. Constants are mirrored from extensions/memory-core/src/dreaming.ts and commented so a future rename in memory-core is a visible drift point here too. * memory/dreaming: tighten cron-token match to known wrapper, not substring The previous match relaxed the line check from 'trimmed line equals token' to 'line contains token anywhere as a substring' to accept the `[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring matching also let any user message embedding the token mid-sentence trigger the dream-promotion hook, and was flagged by both Greptile and Aisle on PR openclaw#70737. Replace it with strip-the-known-prefix-then-exact-match: keep the `[cron:<id>]` wrapper case working, reject every other variant. Add focused unit coverage that the bare token, the wrapped token, and bare multiline cases match while embedded / code-fenced / arbitrarily-wrapped variants do not. * memory/dreaming: drop assistant followup only on assistant-side signals Per PR openclaw#70737 review (aisle-research-bot, Medium): the previous logic suppressed the next assistant message whenever the prior user message matched a 'generated prompt' pattern (`[cron:...]`, `System (untrusted): ...`, heartbeat prompts, exec-completion events). Real users can type those same patterns, which let a user exfiltrate real assistant replies from the dreaming corpus by prefixing their own prompt — the assistant's reply would be silently dropped. Remove the cross-message coupling. Assistant-side machinery (silent replies, system wrappers) is already dropped by sanitizeSessionText, which is the right layer for that filter. Add an explicit assistant-side HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop working without depending on the prior user message. Add a regression test exercising the spoofing scenario. * doctor/cron: assert mirrored dreaming constants stay in sync Per PR openclaw#70737 review (greptile-apps): the doctor migration mirrors three constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG, DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts. A future rename in either file would silently break the migration. Add a vitest unit that reads both files and asserts the literals match. Manually verified the assertion fires with a clear error when one side diverges. Adds no runtime cost; sits in the regular test pipeline. * fix(memory): stabilize dreaming CI checks * memory/dreaming: skip eager narrative session cleanup when detached Per PR openclaw#70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases called deleteNarrativeSessionBestEffort synchronously right after each phase. Once narrative generation moved to detached mode (queued via queueMicrotask), the eager cleanup races the writer: the session is deleted before the queued subagent run reads it, silently dropping cron diary entries. Skip the eager cleanup branch when params.detachNarratives is true. generateAndAppendDreamNarrative still runs its own deleteSession in the finally{} block, so the cleanup intent is preserved without the race. Heartbeat-driven (non-detached) runs keep the original eager-cleanup behavior. * fix(plugin-sdk): restore heartbeat-summary re-export Per PR openclaw#70737 review (chatgpt-codex-connector, P1): the revert of PR openclaw#69875 dropped the `heartbeat-summary` re-export from `openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two days earlier, so removing it is technically a breaking change to a public SDK surface — third-party plugins importing `isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this path would fail with no replacement contract introduced. Restore the re-export. Costs nothing to keep; the helpers are already public via `../infra/heartbeat-summary.ts`. SDK additions are by default backwards-compatible (CLAUDE.md), so removing within days of introduction violates that intent. * changelog: note dreaming decoupling from heartbeat Refs PR openclaw#70737. --------- Co-authored-by: Josh Lehman <[email protected]>
Summary
Surface a
blockedline in bothopenclaw memory statusand/dreaming status|on|offwhen dreaming is enabled but heartbeat formainis not firing — currently fails silently./dreamingslash command now flag a blocked cron, instead of appearing healthy while silently not running.Refs #69811, #46046.
Scope note
Observability only, not execution semantics. The cron hardcodes
sessionTarget: "main"regardless of the configured default agent — so if a user setsdefault: trueon a non-main agent, dreaming still silently never runs. Fixing that is a behavior change to the cron target itself and belongs in a follow-up.Test plan
pnpm test extensions/memory-core(503 pass)pnpm tsgo:prod,pnpm lint:{core,extensions},pnpm plugin-sdk:api:checkheartbeat.every: "0m"— renders as expected