memory/dreaming: decouple managed cron from heartbeat#70737
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".
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".
|
To use Codex here, create a Codex account and connect to github. |
7516389 to
81775df
Compare
* 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]>
…nclaw#70950) * 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 openclaw#70940 and identified in PR openclaw#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 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.
…aming corpus
Cron and heartbeat injections that land inside a regular agent:X:main
transcript previously had only their '[cron:...]' / 'System: ...' user
prompt dropped by sanitize patterns, while the paired assistant ack
('HEARTBEAT_OK', 'Querying MoviePilot:', weekly-summary chatter) still
leaked into the dreaming corpus.
Mark those user records at write time with the existing
provenance.kind = 'internal_system' (the kind was defined in
input-provenance.ts but had no writers). The dreaming corpus ingestion
in session-files.buildSessionEntry now treats this as a record-level
metadata signal — never a user-controlled content match — and drops
both the user record and its paired assistant reply.
Detection is structural so a user-typed '[cron:fakeId]' prompt cannot
weaponize the pair-drop (PR openclaw#70737 review threat model holds).
Note: A-only fix; existing transcripts written before this commit lack
the provenance field, so historical cron/heartbeat assistant chatter
already in the corpus must be cleared separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* 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]>
…nclaw#70950) * 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 openclaw#70940 and identified in PR openclaw#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 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.
* 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]>
…nclaw#70950) * 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 openclaw#70940 and identified in PR openclaw#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 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.
* 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]>
…nclaw#70950) * 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 openclaw#70940 and identified in PR openclaw#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 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.
* 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]>
…nclaw#70950) * 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 openclaw#70940 and identified in PR openclaw#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 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.
* 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]>
…nclaw#70950) * 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 openclaw#70940 and identified in PR openclaw#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 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.
The dreaming ingestion pipeline filters heartbeat user messages (containing [OpenClaw heartbeat poll]) via sanitizeSessionText, but the paired assistant response is only dropped when it is the exact string HEARTBEAT_OK. Local models frequently respond with natural-language acknowledgments (e.g. "Heartbeat received. Main is active.") which pass through the sanitizer unchanged and enter the dream corpus as low-confidence (0.58) memory snippets. Fix: track heartbeat user message drops in buildSessionEntry and skip the immediately following assistant response. This cross-message coupling is safe because the heartbeat prompt pattern is injected by the runtime and cannot be spoofed by user input, unlike [cron:...] or System (untrusted): ... patterns (see PR openclaw#70737). Refs: openclaw#103720
The dreaming ingestion pipeline filters heartbeat user messages (containing [OpenClaw heartbeat poll]) via sanitizeSessionText, but the paired assistant response is only dropped when it is the exact string HEARTBEAT_OK. Local models frequently respond with natural-language acknowledgments (e.g. "Heartbeat received. Main is active.") which pass through the sanitizer unchanged and enter the dream corpus as low-confidence (0.58) memory snippets. Fix: track heartbeat user message drops in buildSessionEntry and skip the immediately following assistant response. This cross-message coupling is safe because the heartbeat prompt pattern is injected by the runtime and cannot be spoofed by user input, unlike [cron:...] or System (untrusted): ... patterns (see PR openclaw#70737). Refs: openclaw#103720
The dreaming ingestion pipeline filters heartbeat user messages (containing [OpenClaw heartbeat poll]) via sanitizeSessionText, but the paired assistant response is only dropped when it is the exact string HEARTBEAT_OK. Local models frequently respond with natural-language acknowledgments (e.g. "Heartbeat received. Main is active.") which pass through the sanitizer unchanged and enter the dream corpus as low-confidence (0.58) memory snippets. Fix: track heartbeat user message drops in buildSessionEntry and skip the immediately following assistant response. This cross-message coupling is safe because the heartbeat prompt pattern is injected by the runtime and cannot be spoofed by user input, unlike [cron:...] or System (untrusted): ... patterns (see PR openclaw#70737). Refs: openclaw#103720
The dreaming ingestion pipeline filters heartbeat user messages (containing [OpenClaw heartbeat poll]) via sanitizeSessionText, but the paired assistant response is only dropped when it is the exact string HEARTBEAT_OK. Local models frequently respond with natural-language acknowledgments (e.g. "Heartbeat received. Main is active.") which pass through the sanitizer unchanged and enter the dream corpus as low-confidence (0.58) memory snippets. Fix: track heartbeat user message drops in buildSessionEntry and skip the immediately following assistant response. This cross-message coupling is safe because the heartbeat prompt pattern is injected by the runtime and cannot be spoofed by user input, unlike [cron:...] or System (untrusted): ... patterns (see PR openclaw#70737). Refs: openclaw#103720
The dreaming ingestion pipeline filters heartbeat user messages (containing [OpenClaw heartbeat poll]) via sanitizeSessionText, but the paired assistant response is only dropped when it is the exact string HEARTBEAT_OK. Local models frequently respond with natural-language acknowledgments (e.g. "Heartbeat received. Main is active.") which pass through the sanitizer unchanged and enter the dream corpus as low-confidence (0.58) memory snippets. Fix: track heartbeat user message drops in buildSessionEntry and skip the immediately following assistant response. This cross-message coupling is safe because the heartbeat prompt pattern is injected by the runtime and cannot be spoofed by user input, unlike [cron:...] or System (untrusted): ... patterns (see PR openclaw#70737). Refs: openclaw#103720
…109403) * fix(dreaming): drop heartbeat assistant responses from dream corpus The dreaming ingestion pipeline filters heartbeat user messages (containing [OpenClaw heartbeat poll]) via sanitizeSessionText, but the paired assistant response is only dropped when it is the exact string HEARTBEAT_OK. Local models frequently respond with natural-language acknowledgments (e.g. "Heartbeat received. Main is active.") which pass through the sanitizer unchanged and enter the dream corpus as low-confidence (0.58) memory snippets. Fix: track heartbeat user message drops in buildSessionEntry and skip the immediately following assistant response. This cross-message coupling is safe because the heartbeat prompt pattern is injected by the runtime and cannot be spoofed by user input, unlike [cron:...] or System (untrusted): ... patterns (see PR #70737). Refs: #103720 * test(dreaming): add heartbeat assistant response filter test * fix(dreaming): use provenance-based heartbeat detection instead of text matching Replace text-based heartbeat detection (isGeneratedHeartbeatPromptMessage) with provenance-based authentication. The heartbeat turn now carries provenance: { kind: "heartbeat" } when persisted to the transcript, and buildSessionEntry checks for this provenance instead of matching user-spoofable text content. Changes: - src/sessions/input-provenance.ts: Add "heartbeat" to INPUT_PROVENANCE_KIND_VALUES - src/auto-reply/reply/get-reply-run.ts: Attach heartbeat provenance to user turn input when isHeartbeat is true - packages/memory-host-sdk/src/host/session-files.ts: Check message.provenance.kind === "heartbeat" instead of text matching - packages/memory-host-sdk/src/host/session-files.test.ts: Update heartbeat test to include provenance; add lookalike regression test This addresses the ClawSweeper review finding [P1] about user-spoofable text matching. The cross-message coupling is now authenticated by runtime provenance, not user-typed content. Refs: #103720 * fix(dreaming): add targeted heartbeat-derived corpus repair * fix(dreaming): fix TS return type for clearScopedLegacySessionIngestionJson catch * fix(dreaming): match chunked SQLite seen-state keys by stored scope * fix(dreaming): repair SQLite-backed session transcript lookup in doctor The findHeartbeatContaminatedCorpusLines function could not read SQLite-backed session transcripts. When a corpus ref had no .jsonl extension (SQLite logical path format), it appended .jsonl, read an empty file, and silently skipped — leaving pre-fix heartbeat contamination intact for SQLite users. Fix: - Add loadTranscriptLinesFromSqlite helper that reads events via the canonical loadTranscriptEventsSync API - Serialize all events to preserve original event positions for corpus #L<n> line-number references - Fall back to SQLite reader when filesystem read fails on a non-.jsonl corpus ref path Test: add SQLite-backed session regression test that seeds a session, writes corpus refs without .jsonl extension, and verifies both audit detection and targeted repair. * fix(dreaming): address autoreview P1 and P2 for checkpoint clearing and early return P1: Remove clearScopedSessionIngestionState call after heartbeat line removal. Clearing the cursor causes the next ingestion to re-process the transcript from the beginning, duplicating normal corpus lines that were deliberately retained. P2: Remove early return after heartbeat cleanup so the self-ingestion narrative content check and archiveDiary request still run. * fix(dreaming): resolve corpus ref session path without duplicate agent ID The session path in corpus refs is already in the format 'sessions/main/abc.jsonl' (includes agent subdirectory). The previous code prepended source.agentId again, creating a path like 'agents/main/sessions/main/abc.jsonl' — which silently returned empty in production workspaces. Fix: use path.basename to extract just the filename from the session path, then construct the transcript path correctly. * refactor(dreaming): split repair utils into separate file to satisfy LOC ratchet The dreaming-repair.ts file grew from 337 to 831 lines, exceeding the 500-line LOC ratchet limit. Split helper utilities into a new dreaming-repair-utils.ts file: - dreaming-repair.ts (365 lines): imports, types, public API functions (auditDreamingArtifacts, repairDreamingArtifacts) - dreaming-repair-utils.ts (498 lines): all helper functions, constants, and internal types * fix(ts): add missing type imports for return types in dreaming-repair.ts * fix(deadcode): remove unused exports from dreaming-repair-utils.ts * fix: resolve merge conflict marker in get-reply-run.ts * fix: remove unnecessary export from INPUT_PROVENANCE_KIND_VALUES * fix(dreaming): converge targeted repair path with wholesale archive+clear When heartbeat contamination is found, archive the entire session-corpus directory and clear all SQLite checkpoints instead of doing targeted line-by-line rewrite. This ensures the cleaned corpus gets re-ingested under the new provenance-based forward filter. * fix(dreaming): authenticate heartbeat transcript turns Co-authored-by: Erick Kinnee <[email protected]> --------- Co-authored-by: Erick Kinnee <[email protected]> Co-authored-by: Erick Kinnee <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…penclaw#109403) * fix(dreaming): drop heartbeat assistant responses from dream corpus The dreaming ingestion pipeline filters heartbeat user messages (containing [OpenClaw heartbeat poll]) via sanitizeSessionText, but the paired assistant response is only dropped when it is the exact string HEARTBEAT_OK. Local models frequently respond with natural-language acknowledgments (e.g. "Heartbeat received. Main is active.") which pass through the sanitizer unchanged and enter the dream corpus as low-confidence (0.58) memory snippets. Fix: track heartbeat user message drops in buildSessionEntry and skip the immediately following assistant response. This cross-message coupling is safe because the heartbeat prompt pattern is injected by the runtime and cannot be spoofed by user input, unlike [cron:...] or System (untrusted): ... patterns (see PR openclaw#70737). Refs: openclaw#103720 * test(dreaming): add heartbeat assistant response filter test * fix(dreaming): use provenance-based heartbeat detection instead of text matching Replace text-based heartbeat detection (isGeneratedHeartbeatPromptMessage) with provenance-based authentication. The heartbeat turn now carries provenance: { kind: "heartbeat" } when persisted to the transcript, and buildSessionEntry checks for this provenance instead of matching user-spoofable text content. Changes: - src/sessions/input-provenance.ts: Add "heartbeat" to INPUT_PROVENANCE_KIND_VALUES - src/auto-reply/reply/get-reply-run.ts: Attach heartbeat provenance to user turn input when isHeartbeat is true - packages/memory-host-sdk/src/host/session-files.ts: Check message.provenance.kind === "heartbeat" instead of text matching - packages/memory-host-sdk/src/host/session-files.test.ts: Update heartbeat test to include provenance; add lookalike regression test This addresses the ClawSweeper review finding [P1] about user-spoofable text matching. The cross-message coupling is now authenticated by runtime provenance, not user-typed content. Refs: openclaw#103720 * fix(dreaming): add targeted heartbeat-derived corpus repair * fix(dreaming): fix TS return type for clearScopedLegacySessionIngestionJson catch * fix(dreaming): match chunked SQLite seen-state keys by stored scope * fix(dreaming): repair SQLite-backed session transcript lookup in doctor The findHeartbeatContaminatedCorpusLines function could not read SQLite-backed session transcripts. When a corpus ref had no .jsonl extension (SQLite logical path format), it appended .jsonl, read an empty file, and silently skipped — leaving pre-fix heartbeat contamination intact for SQLite users. Fix: - Add loadTranscriptLinesFromSqlite helper that reads events via the canonical loadTranscriptEventsSync API - Serialize all events to preserve original event positions for corpus #L<n> line-number references - Fall back to SQLite reader when filesystem read fails on a non-.jsonl corpus ref path Test: add SQLite-backed session regression test that seeds a session, writes corpus refs without .jsonl extension, and verifies both audit detection and targeted repair. * fix(dreaming): address autoreview P1 and P2 for checkpoint clearing and early return P1: Remove clearScopedSessionIngestionState call after heartbeat line removal. Clearing the cursor causes the next ingestion to re-process the transcript from the beginning, duplicating normal corpus lines that were deliberately retained. P2: Remove early return after heartbeat cleanup so the self-ingestion narrative content check and archiveDiary request still run. * fix(dreaming): resolve corpus ref session path without duplicate agent ID The session path in corpus refs is already in the format 'sessions/main/abc.jsonl' (includes agent subdirectory). The previous code prepended source.agentId again, creating a path like 'agents/main/sessions/main/abc.jsonl' — which silently returned empty in production workspaces. Fix: use path.basename to extract just the filename from the session path, then construct the transcript path correctly. * refactor(dreaming): split repair utils into separate file to satisfy LOC ratchet The dreaming-repair.ts file grew from 337 to 831 lines, exceeding the 500-line LOC ratchet limit. Split helper utilities into a new dreaming-repair-utils.ts file: - dreaming-repair.ts (365 lines): imports, types, public API functions (auditDreamingArtifacts, repairDreamingArtifacts) - dreaming-repair-utils.ts (498 lines): all helper functions, constants, and internal types * fix(ts): add missing type imports for return types in dreaming-repair.ts * fix(deadcode): remove unused exports from dreaming-repair-utils.ts * fix: resolve merge conflict marker in get-reply-run.ts * fix: remove unnecessary export from INPUT_PROVENANCE_KIND_VALUES * fix(dreaming): converge targeted repair path with wholesale archive+clear When heartbeat contamination is found, archive the entire session-corpus directory and clear all SQLite checkpoints instead of doing targeted line-by-line rewrite. This ensures the cleaned corpus gets re-ingested under the new provenance-based forward filter. * fix(dreaming): authenticate heartbeat transcript turns Co-authored-by: Erick Kinnee <[email protected]> --------- Co-authored-by: Erick Kinnee <[email protected]> Co-authored-by: Erick Kinnee <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
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