Skip to content

memory/dreaming: decouple managed cron from heartbeat#70737

Merged
Patrick-Erichsen merged 22 commits intoopenclaw:mainfrom
Patrick-Erichsen:patrick/dreaming-isolated-cron
Apr 24, 2026
Merged

memory/dreaming: decouple managed cron from heartbeat#70737
Patrick-Erichsen merged 22 commits intoopenclaw:mainfrom
Patrick-Erichsen:patrick/dreaming-isolated-cron

Conversation

@Patrick-Erichsen
Copy link
Copy Markdown
Contributor

@Patrick-Erichsen Patrick-Erichsen commented Apr 23, 2026

Summary

Cherry-picks 13 commits from @jalehman's josh/dreaming-isolated-cron-fix branch to move the managed dreaming cron off the heartbeat path and onto isolated agent turns (sessionTarget: "isolated" + payload.kind: "agentTurn" + lightContext: true), so dreaming runs even when heartbeat is disabled for the default agent. Also reverts #69875 (blocked-reason observability becomes dead code once decoupling lands) and adds a doctor migration for stale main-session dreaming jobs in persisted cron configs.

Issues addressed

Commits picked from Josh's branch (oldest → newest)

  • 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

Test plan

  • pnpm tsgo / pnpm tsgo:extensions / pnpm check:test-types
  • pnpm lint:core
  • pnpm test extensions/memory-core — 48 files, 515 passed
  • pnpm test src/commands/doctor-cron.test.ts src/commands/doctor-cron-dreaming-payload-migration.test.ts
  • Manual: run openclaw doctor --fix against a config with the legacy sessionTarget: "main" shape and confirm rewrite
  • Manual: run dreaming end-to-end with heartbeat disabled and confirm a diary entry lands

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime extensions: memory-core Extension: memory-core commands Command implementations agents Agent runtime and tooling size: XL maintainer Maintainer-authored PR labels Apr 23, 2026
@Patrick-Erichsen Patrick-Erichsen force-pushed the patrick/dreaming-isolated-cron branch 3 times, most recently from 87e511f to b03e9b7 Compare April 23, 2026 23:15
@Patrick-Erichsen Patrick-Erichsen marked this pull request as ready for review April 23, 2026 23:58
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 23, 2026

🔒 Aisle Security Analysis

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

# Severity Title
1 🟠 High Plugins can disable bootstrap guardrails for subagent runs via lightContext/lightweight mode
2 🟡 Medium User-controlled patterns can suppress user messages from dreaming corpus (log/forensics evasion)
3 🟡 Medium Potential orphaned subagent runs due to immediate session deletion after narrative timeout
1. 🟠 Plugins can disable bootstrap guardrails for subagent runs via lightContext/lightweight mode
Property Value
Severity High
CWE CWE-285
Location src/gateway/server-plugins.ts:331-347

Description

createGatewaySubagentRuntime().run() forwards the plugin-provided lightContext: true flag into the gateway agent call as bootstrapContextMode: "lightweight" without any authorization/allowlist checks.

In bootstrap-files.ts, bootstrapContextMode: "lightweight" for non-heartbeat runs intentionally empties the bootstrap context (returns []). This can drop workspace bootstrap policies/guardrails that would otherwise be injected for the run.

Impact:

  • Any plugin that can call the subagent runtime can request a lightweight bootstrap and thereby bypass workspace bootstrap files (often used for safety policy, tool-use constraints, data handling rules, etc.).
  • This creates a guardrail-bypass surface and can increase the risk of data leakage or unsafe tool use by subagents, depending on what your bootstrap files enforce.

Vulnerable code:

...(params.lightContext === true && { bootstrapContextMode: "lightweight" }),

and the behavior of lightweight mode:

// cron/default lightweight mode keeps bootstrap context empty on purpose.
return [];

Recommendation

Restrict who can request bootstrapContextMode: "lightweight" (or remove this surface from plugins).

Options:

  • Allowlist specific trusted plugins/lanes to use lightweight mode.
  • Require an explicit capability/permission on the plugin scope (similar to model override authorization).
  • Consider making lightweight mode only available to internal callers (non-plugin) and keep plugin subagent runs always in full mode.

Example (capability gate):

const canUseLightContext = scope?.client && canClientUseLightContext(scope.client);
if (params.lightContext && !canUseLightContext) {
  throw new Error("lightContext/lightweight bootstrap is not authorized for this plugin.");
}

const payload = await dispatchGatewayMethod("agent", {// ...
  ...(params.lightContext === true && canUseLightContext
    ? { bootstrapContextMode: "lightweight" as const }
    : {}),
});

Also document clearly what is removed in lightweight mode and ensure any mandatory safety/tooling restrictions are enforced outside of bootstrap context so they cannot be bypassed by context mode changes.

2. 🟡 User-controlled patterns can suppress user messages from dreaming corpus (log/forensics evasion)
Property Value
Severity Medium
CWE CWE-778
Location src/memory-host-sdk/host/session-files.ts:345-396

Description

sanitizeSessionText drops user-role messages that match patterns intended for internal/system-generated traffic (system wrapper, cron prompt, heartbeat prompt), and also strips internal-runtime-context blocks before normalization.

This creates an evasion primitive for any downstream feature that relies on the derived dreaming corpus (summarization, search, safety analysis, auditing, or automated controls):

  • Spoofable prefixes cause message omission: a user can start their message with either System ...: [...] or [cron:...] and their content will be omitted from the corpus because the match is based solely on message text (not provenance).
  • User-injectable internal-context delimiters can erase content: stripInternalRuntimeContext will remove any blocks delimited by <<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>/<<<END_OPENCLAW_INTERNAL_CONTEXT>>> if they appear on their own lines. A user can include these delimiters in their message to strip arbitrary parts of their own text before it reaches the corpus.

Vulnerable logic:

const strippedInternal = stripInternalRuntimeContext(strippedInbound);
...
if (isGeneratedSystemWrapperMessage(normalized, role)) return null;
if (isGeneratedCronPromptMessage(normalized, role)) return null;
if (isGeneratedHeartbeatPromptMessage(normalized, role)) return null;

While raw transcripts may still exist on disk, the change introduces a clear bypass of the processed dataset that other security/monitoring features may depend on.

Recommendation

Avoid using user-controllable text patterns alone to classify/omit messages from the dreaming corpus.

Options (can be combined):

  1. Tag messages at ingestion time with trusted provenance (e.g., message.metadata.generatedBy = "cron"|"system_wrapper"|"heartbeat") and only drop when that trusted flag is present.

  2. If provenance metadata is not available, do not drop the entire user message on these patterns; instead, remove only the wrapper prefix and keep the remainder (or keep the message but mark it as suspected_generated=true).

  3. For stripInternalRuntimeContext, only strip when a trusted metadata flag indicates the block was injected by the runtime (or escape delimiters on user input using escapeInternalRuntimeContextDelimiters).

Example safer approach:

function sanitizeSessionText(text: string, role: "user"|"assistant", meta?: { generatedByRuntime?: boolean }): string | null {
  const strippedInbound = stripInboundMetadataForUserRole(text, role);
  const strippedInternal = meta?.generatedByRuntime ? stripInternalRuntimeContext(strippedInbound)
                                                  : escapeInternalRuntimeContextDelimiters(strippedInbound);
  const normalized = normalizeSessionText(strippedInternal);
  if (!normalized) return null;// Only drop wrapper/prompt messages when they are known runtime-generated.
  if (role === "user" && meta?.generatedByRuntime && (GENERATED_SYSTEM_MESSAGE_RE.test(normalized) || DIRECT_CRON_PROMPT_RE.test(normalized))) {
    return null;
  }
  return normalized;
}

This preserves auditability while still filtering genuine runtime noise.

3. 🟡 Potential orphaned subagent runs due to immediate session deletion after narrative timeout
Property Value
Severity Medium
CWE CWE-400
Location extensions/memory-core/src/dreaming-narrative.ts:89-93

Description

generateAndAppendDreamNarrative now waits only 15 seconds for a narrative subagent run and then deletes the subagent session in finally{} regardless of whether the run is still executing.

If waitForRun returns timeout, the function returns early (no narrative written) and then immediately calls deleteSession. Without ensuring the run has completed/cancelled, this can create an orphaned or still-running subagent run that is no longer tracked by the parent, especially when dreaming is triggered via cron and narratives are detached. Repeated cron invocations could accumulate concurrent/orphaned work and degrade availability.

Vulnerable behavior:

  • A short NARRATIVE_TIMEOUT_MS (15s) increases the likelihood of timeouts under load
  • On timeout, the code does not wait for the run to settle/cancel before deleting the session
  • Cron mode additionally detaches narrative generation via queueMicrotask (elsewhere), making these background runs harder to control/limit

Vulnerable code:

const result = await params.subagent.waitForRun({ runId, timeoutMs: NARRATIVE_TIMEOUT_MS });
if (result.status !== "ok") {
  return;
}
...
} finally {
  if (params.subagent) {
    await params.subagent.deleteSession({ sessionKey });
  }
}

Recommendation

Ensure timed-out narrative runs are explicitly settled/cancelled before deleting the session, or extend the cleanup to wait for completion with a bounded settle timeout.

For example:

const result = await subagent.waitForRun({ runId, timeoutMs: NARRATIVE_TIMEOUT_MS });
if (result.status === "timeout") {// Option A: attempt cancellation if supported
  await subagent.cancelRun?.({ runId }).catch(() => undefined);// Option B: bounded settle wait to avoid deleting while still running
  await subagent.waitForRun({ runId, timeoutMs: 120_000 }).catch(() => undefined);
}// Only then delete session
await subagent.deleteSession({ sessionKey });

Additionally, when cron detaches narratives, keep a small concurrency limit (e.g., a per-workspace queue) and log/track failures instead of swallowing them, to avoid unbounded background work buildup.


Analyzed PR: #70737 at commit bed48c1

Last updated on: 2026-04-24T05:12:02Z

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR decouples managed dreaming cron from the heartbeat path by switching the cron payload from sessionTarget: "main" + kind: "systemEvent" to sessionTarget: "isolated" + kind: "agentTurn" + lightContext: true, so dreaming runs independently of whether heartbeat is enabled. It also adds a doctor migration to rewrite stale persisted cron configs, filters cron/heartbeat/archive transcript noise from the dreaming corpus, removes the now-dead heartbeat-blocked-reason observability code, and reduces the narrative subagent timeout while detaching diary generation from cron completion via queueMicrotask.

Confidence Score: 5/5

Safe to merge — no P0/P1 issues found; all findings are P2 style/quality suggestions.

The architectural change is well-scoped: the new isolated cron path is gated by the same distinctive system event token as the old heartbeat path, the before_agent_reply early-exit logic is correct, the doctor migration covers stale persisted configs, and the test suite (515 passing tests + new targeted tests) gives good coverage. Remaining comments cover a broadened substring match, fire-and-forget narrative error handling, an aggressive 15-second timeout, and duplicated constants — all P2.

No files require special attention; reviewers may want to revisit dreaming-shared.ts if shorter event tokens are added in the future.

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

Comment:
**Broader substring match for system event tokens**

The added `|| line.includes(normalizedEventText)` relaxes the match from "trimmed line equals token exactly" to "line contains the token anywhere as a substring." This is intentional for the `[cron:...] __token__` prefix case, but it means any message that embeds the token mid-sentence would also trigger dreaming. The `__openclaw_memory_core_short_term_promotion_dream__` token is distinctive enough that false positives are unlikely in practice, but a narrower fix — stripping the known `[cron:…]` prefix before comparing — would be more precise and avoid any future ambiguity if shorter tokens are added.

```suggestion
  return normalizedBody
    .split(/\r?\n/)
    .some(
      (line) =>
        line.trim() === normalizedEventText ||
        line.replace(/^\[cron:[^\]]+\]\s*/, "").trim() === normalizedEventText,
    );
```

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

---

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

Comment:
**Fire-and-forget narrative tasks drop errors silently**

When `detachNarratives` is true, `generateAndAppendDreamNarrative` is kicked off via `queueMicrotask` with `.catch(() => undefined)`. Internal errors are logged by the function itself via `params.logger.warn`, so they're not fully invisible, but an uncaught rejection that escapes the function's own try/catch (e.g. from the subagent binding teardown) would be silently discarded here. If the intent is best-effort, naming the suppression makes the intent clearer. The same pattern occurs in `runRemDreaming` and in `runShortTermDreamingPromotionIfTriggered`.

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

---

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

Comment:
**15-second narrative timeout may be aggressive for cold subagent starts**

The timeout was reduced from 60 s to 15 s. For a detached cron path the parent already returned, so a stalled subagent has no blocking effect on the cron run. The only consequence of a 15-second miss is a skipped diary entry, which is intentionally best-effort — but cold subagent starts (model round-trip + auth) can exceed 15 s under load. Consider 30 s as a middle ground, or make the value configurable via the `lightContext` seam added here.

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

---

This is a comment left during a code review.
Path: src/commands/doctor-cron-dreaming-payload-migration.ts
Line: 1-12

Comment:
**Mirrored constants create a long-term sync risk**

The comment acknowledges that `MANAGED_DREAMING_CRON_NAME`, `MANAGED_DREAMING_CRON_TAG`, and `DREAMING_SYSTEM_EVENT_TEXT` are duplicated from `extensions/memory-core/src/dreaming.ts`. A future rename in one location without updating the other would silently break the doctor migration. Consider a lint rule or at minimum a CI grep assertion that the values match, to make the divergence detectable without manual auditing.

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

Reviews (1): Last reviewed commit: "doctor/cron: migrate stale managed dream..." | Re-trigger Greptile

Comment thread extensions/memory-core/src/dreaming-shared.ts Outdated
Comment thread extensions/memory-core/src/dreaming-phases.ts
Comment thread extensions/memory-core/src/dreaming-narrative.ts
Comment thread src/commands/doctor-cron-dreaming-payload-migration.ts
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b03e9b765b

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread extensions/memory-core/src/dreaming.ts
Comment thread extensions/memory-core/src/dreaming-phases.ts
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
…ring

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.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 488cfb6c72

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread src/plugin-sdk/infra-runtime.ts
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
@Patrick-Erichsen Patrick-Erichsen force-pushed the patrick/dreaming-isolated-cron branch from 7516389 to 81775df Compare April 24, 2026 05:01
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
…ring

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.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
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.
jalehman and others added 20 commits April 23, 2026 22:08
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.
…ring

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.
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.
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.
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.
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.
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bed48c1a09

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

): Promise<SessionFileEntry | null> {
try {
const stat = await fs.stat(absPath);
if (shouldSkipTranscriptFileForDreaming(absPath)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict transcript skipping to dreaming-only call sites

buildSessionEntry now short-circuits every .deleted/.checkpoint transcript into an empty entry, but this helper is shared beyond dreaming ingestion (for example extensions/memory-core/src/memory/manager-sync-ops.ts uses it to index session content and extensions/memory-core/src/memory/qmd-manager.ts uses it for session exports). Because listSessionFilesForAgent still returns reset/deleted session files, those callers will now reindex/export historical transcripts as empty content, effectively dropping searchable/exported history after resets/deletes. The skip should be scoped behind an option or moved to the dreaming ingestion path instead of the shared parser.

Useful? React with 👍 / 👎.

@Patrick-Erichsen Patrick-Erichsen merged commit aca92b2 into openclaw:main Apr 24, 2026
65 checks passed
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts
for the CLI runner. Asserts:

1. When trigger=cron and a before_agent_reply hook claims the turn
   (handled: true), runCliAgent must NOT invoke the codex subprocess and
   must return the hook's reply text in payloads[0].
2. When the hook claims without a reply body, the synthesized payload
   uses SILENT_REPLY_TOKEN.
3. Non-cron triggers do not invoke the hook (no behavior change for
   normal user/heartbeat traffic).
4. Without a registered hook, falls through to the CLI subprocess.

Currently fails (RED): tests 1 and 2 fail because runCliAgent never
fires before_agent_reply — the hook gate exists only in the embedded PI
runner (src/agents/pi-embedded-runner/run.ts:326). This is the
CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in
PR openclaw#70737 review.

Next commit: implement the hook gate in runPreparedCliAgent (GREEN).
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
…urns

Mirrors the embedded PI runner gate from
src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs
(notably memory-core dreaming) can short-circuit a CLI-backed agent
turn before the codex/claude/gemini subprocess is spawned.

Without this, configuring a default agent's model to a CLI backend
(codex-cli, claude-cli, gemini-cli, or any third-party
`registerCliBackend` provider) silently broke dreaming: the cron
sentinel was sent to the underlying LLM as a literal user prompt and
the dreaming hook never executed. See openclaw#70940 for the
empirical repro (codex-cli observed sending the dream-token to GPT-5.5
with no `memory-core: dreaming promotion complete` line).

Also extracts `buildHandledReplyPayloads` locally; eventually that
should be unified with the embedded PI runner's helper, but that's a
mechanical refactor for a follow-up.

Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only
useful if cron-driven dreaming exists, which is what openclaw#70737 introduces.

TDD trail:
- prior commit: RED test asserting the hook gate (4 cases)
- this commit: implementation that turns those tests green (4/4 pass).

Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts
4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean
on touched files; pre-existing tsgo failure in
src/plugin-sdk/provider-tools.ts is unrelated to these changes.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
…urns

Mirrors the embedded PI runner gate from
src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs
(notably memory-core dreaming) can short-circuit a CLI-backed agent
turn before the codex/claude/gemini subprocess is spawned.

Without this, configuring a default agent's model to a CLI backend
(codex-cli, claude-cli, gemini-cli, or any third-party
`registerCliBackend` provider) silently broke dreaming: the cron
sentinel was sent to the underlying LLM as a literal user prompt and
the dreaming hook never executed. See openclaw#70940 for the
empirical repro (codex-cli observed sending the dream-token to GPT-5.5
with no `memory-core: dreaming promotion complete` line).

Also extracts `buildHandledReplyPayloads` locally; eventually that
should be unified with the embedded PI runner's helper, but that's a
mechanical refactor for a follow-up.

Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only
useful if cron-driven dreaming exists, which is what openclaw#70737 introduces.

TDD trail:
- prior commit: RED test asserting the hook gate (4 cases)
- this commit: implementation that turns those tests green (4/4 pass).

Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts
4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean
on touched files; pre-existing tsgo failure in
src/plugin-sdk/provider-tools.ts is unrelated to these changes.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
…urns

Mirrors the embedded PI runner gate from
src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs
(notably memory-core dreaming) can short-circuit a CLI-backed agent
turn before the codex/claude/gemini subprocess is spawned.

Without this, configuring a default agent's model to a CLI backend
(codex-cli, claude-cli, gemini-cli, or any third-party
`registerCliBackend` provider) silently broke dreaming: the cron
sentinel was sent to the underlying LLM as a literal user prompt and
the dreaming hook never executed. See openclaw#70940 for the
empirical repro (codex-cli observed sending the dream-token to GPT-5.5
with no `memory-core: dreaming promotion complete` line).

Also extracts `buildHandledReplyPayloads` locally; eventually that
should be unified with the embedded PI runner's helper, but that's a
mechanical refactor for a follow-up.

Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only
useful if cron-driven dreaming exists, which is what openclaw#70737 introduces.

TDD trail:
- prior commit: RED test asserting the hook gate (4 cases)
- this commit: implementation that turns those tests green (4/4 pass).

Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts
4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean
on touched files; pre-existing tsgo failure in
src/plugin-sdk/provider-tools.ts is unrelated to these changes.
Patrick-Erichsen added a commit to Patrick-Erichsen/openclaw that referenced this pull request Apr 24, 2026
…urns

Mirrors the embedded PI runner gate from
src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs
(notably memory-core dreaming) can short-circuit a CLI-backed agent
turn before the codex/claude/gemini subprocess is spawned.

Without this, configuring a default agent's model to a CLI backend
(codex-cli, claude-cli, gemini-cli, or any third-party
`registerCliBackend` provider) silently broke dreaming: the cron
sentinel was sent to the underlying LLM as a literal user prompt and
the dreaming hook never executed. See openclaw#70940 for the
empirical repro (codex-cli observed sending the dream-token to GPT-5.5
with no `memory-core: dreaming promotion complete` line).

Also extracts `buildHandledReplyPayloads` locally; eventually that
should be unified with the embedded PI runner's helper, but that's a
mechanical refactor for a follow-up.

Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only
useful if cron-driven dreaming exists, which is what openclaw#70737 introduces.

TDD trail:
- prior commit: RED test asserting the hook gate (4 cases)
- this commit: implementation that turns those tests green (4/4 pass).

Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts
4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean
on touched files; pre-existing tsgo failure in
src/plugin-sdk/provider-tools.ts is unrelated to these changes.
Patrick-Erichsen added a commit that referenced this pull request Apr 24, 2026
)

* test(cli-runner): RED — assert before_agent_reply fires on cron triggers

Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts
for the CLI runner. Asserts:

1. When trigger=cron and a before_agent_reply hook claims the turn
   (handled: true), runCliAgent must NOT invoke the codex subprocess and
   must return the hook's reply text in payloads[0].
2. When the hook claims without a reply body, the synthesized payload
   uses SILENT_REPLY_TOKEN.
3. Non-cron triggers do not invoke the hook (no behavior change for
   normal user/heartbeat traffic).
4. Without a registered hook, falls through to the CLI subprocess.

Currently fails (RED): tests 1 and 2 fail because runCliAgent never
fires before_agent_reply — the hook gate exists only in the embedded PI
runner (src/agents/pi-embedded-runner/run.ts:326). This is the
CLI-backed-agent dreaming gap reported in #70940 and identified in
PR #70737 review.

Next commit: implement the hook gate in runPreparedCliAgent (GREEN).

* fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns

Mirrors the embedded PI runner gate from
src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs
(notably memory-core dreaming) can short-circuit a CLI-backed agent
turn before the codex/claude/gemini subprocess is spawned.

Without this, configuring a default agent's model to a CLI backend
(codex-cli, claude-cli, gemini-cli, or any third-party
`registerCliBackend` provider) silently broke dreaming: the cron
sentinel was sent to the underlying LLM as a literal user prompt and
the dreaming hook never executed. See #70940 for the
empirical repro (codex-cli observed sending the dream-token to GPT-5.5
with no `memory-core: dreaming promotion complete` line).

Also extracts `buildHandledReplyPayloads` locally; eventually that
should be unified with the embedded PI runner's helper, but that's a
mechanical refactor for a follow-up.

Closes #70940 once both this PR and #70737 land — this fix is only
useful if cron-driven dreaming exists, which is what #70737 introduces.

TDD trail:
- prior commit: RED test asserting the hook gate (4 cases)
- this commit: implementation that turns those tests green (4/4 pass).

Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts
4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean
on touched files; pre-existing tsgo failure in
src/plugin-sdk/provider-tools.ts is unrelated to these changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling commands Command implementations docs Improvements or additions to documentation extensions: memory-core Extension: memory-core gateway Gateway runtime maintainer Maintainer-authored PR size: XL

Projects

None yet

2 participants