memory/dreaming: decouple managed cron from heartbeat#70737
memory/dreaming: decouple managed cron from heartbeat#70737Patrick-Erichsen merged 22 commits intoopenclaw:mainfrom
Conversation
87e511f to
b03e9b7
Compare
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟠 Plugins can disable bootstrap guardrails for subagent runs via lightContext/lightweight mode
Description
In Impact:
Vulnerable code: ...(params.lightContext === true && { bootstrapContextMode: "lightweight" }),and the behavior of lightweight mode: // cron/default lightweight mode keeps bootstrap context empty on purpose.
return [];RecommendationRestrict who can request Options:
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)
Description
This creates an evasion primitive for any downstream feature that relies on the derived dreaming corpus (summarization, search, safety analysis, auditing, or automated controls):
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. RecommendationAvoid using user-controllable text patterns alone to classify/omit messages from the dreaming corpus. Options (can be combined):
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
Description
If Vulnerable behavior:
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 });
}
}RecommendationEnsure 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 Last updated on: 2026-04-24T05:12:02Z |
Greptile SummaryThis PR decouples managed dreaming cron from the heartbeat path by switching the cron payload from Confidence Score: 5/5Safe 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 AIThis 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 |
There was a problem hiding this comment.
💡 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".
…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.
There was a problem hiding this comment.
💡 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".
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.
|
To use Codex here, create a Codex account and connect to github. |
7516389 to
81775df
Compare
…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.
…rom dreaming corpus
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.
81775df to
bed48c1
Compare
There was a problem hiding this comment.
💡 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)) { |
There was a problem hiding this comment.
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 👍 / 👎.
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).
…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.
…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.
…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.
…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.
) * 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.
Summary
Cherry-picks 13 commits from @jalehman's
josh/dreaming-isolated-cron-fixbranch 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
heartbeat.activeHoursbecause the consumer ran inside heartbeat-runner. Once cron drives an isolated turn directly, activeHours no longer applies.sessionTarget: isolatedbut notmain. This PR makes isolated the default/only managed shape; the reporter's manual workaround becomes the shipped behavior.lightContexton plugin-managed dreaming jobs. This PR hardcodeslightContext: trueon the managed cron payload and on the dream-diary subagent run (via a newSubagentRunParams.lightContextSDK seam) rather than exposing it as a memory-core config knob; the underlying cost/context-bloat concern is solved either way.Commits picked from Josh's branch (oldest → newest)
openclaw-3ba.1move managed dreaming cron to isolated agent turnsopenclaw-46dclaim cron runs before embedded attemptsopenclaw-575disable managed dreaming cron deliveryopenclaw-575accept wrapped dreaming cron tokensopenclaw-ccdfilter cron and wrapper transcript noise from dreaming corpusopenclaw-cd9filter archived, cron, and heartbeat transcript noise from dreaming corpusopenclaw-cd9suppress role-label reflection tags in rem dreamingopenclaw-b49stop narrative timeouts from blocking dreaming cronopenclaw-b49keep managed dreaming cron out of diary subagentsopenclaw-ff9restore cron dream diary generation without serial waitsopenclaw-ff9run dreaming narratives with lightweight isolated subagent lanesopenclaw-ff9detach cron dream diary generation from run completionopenclaw-ff9defer cron diary task startup until after cron completionTest plan
pnpm tsgo/pnpm tsgo:extensions/pnpm check:test-typespnpm lint:corepnpm test extensions/memory-core— 48 files, 515 passedpnpm test src/commands/doctor-cron.test.ts src/commands/doctor-cron-dreaming-payload-migration.test.tsopenclaw doctor --fixagainst a config with the legacysessionTarget: "main"shape and confirm rewrite