## Summary Two predicate-level defects in `memory-core` Dreaming's session-corpus ingestion combine to make the corpus on cron-heavy deployments dominated by automation noise. On my workspace (`openclaw@2026.6.1`), 67% of today's corpus and 91% of `2026-05-31`'s corpus are archived `.deleted.*` / `.reset.*` transcripts of cron-spawned subagent sessions — and as a result REM today reported *"No strong patterns surfaced"* and `deep` promoted nothing. These are related to #67272 (closed), #68449 (closed), and #72611 (open), but identify **different** root causes than those issues capture. See "Why this is not a duplicate of #72611" below. ## Defect 1 — Corpus ingestion uses the usage-billing predicate, which includes archived transcripts `listSessionFilesForAgent` filters the agent's session directory using `isUsageCountedSessionTranscriptFileName`: ```js // dist/engine-qmd-oR1jDIms.js:136-140 async function listSessionFilesForAgent(agentId) { ... return (await fs$1.readdir(dir, { withFileTypes: true })) .filter((entry) => entry.isFile()) .map((entry) => entry.name) .filter((name) => isUsageCountedSessionTranscriptFileName(name)) .map((name) => path.join(dir, name)); } ``` That predicate explicitly accepts archive variants: ```js // dist/paths-Bz9nEaho.js:59-69 function isPrimarySessionTranscriptFileName(fileName) { if (fileName === "sessions.json") return false; if (!fileName.endsWith(".jsonl")) return false; if (isTrajectoryRuntimeArtifactName(fileName)) return false; if (isCompactionCheckpointTranscriptFileName(fileName)) return false; return !isSessionArchiveArtifactName(fileName); } function isUsageCountedSessionTranscriptFileName(fileName) { if (isPrimarySessionTranscriptFileName(fileName)) return true; return hasArchiveSuffix(fileName, "reset") || hasArchiveSuffix(fileName, "deleted"); } ``` `isUsageCountedSessionTranscriptFileName` is the right predicate for **usage / billing telemetry** — you absolutely want to keep counting messages from a transcript even after it was rotated or deleted. But it's the wrong predicate for **dreaming corpus ingestion**, because each rotation/reset event causes the same conversation to be re-fed into the corpus a second time as a fresh file. The correct predicate here is `isPrimarySessionTranscriptFileName`. ### Local evidence — archived-vs-total per day ``` 2026-05-31.txt total=33 archived=30 (91%) 2026-06-01.txt total=214 archived=133 (62%) 2026-06-02.txt total=33 archived=8 (24%) 2026-06-03.txt total=112 archived=75 (67%) ``` (Reproduced with `grep -cE "\.jsonl\.(deleted|reset)" memory/.dreams/session-corpus/*.txt`.) ## Defect 2 — Cron classification is keyed on the session's own key, not on parentage `loadSessionTranscriptClassificationForSessionsDir` builds the `cronRunTranscriptPaths` set by checking whether each session's own key matches `isCronRunSessionKey`: ```js // dist/engine-qmd-oR1jDIms.js:94-108 function loadSessionTranscriptClassificationForSessionsDir(sessionsDir) { const store = readSessionTranscriptClassificationStore(path.join(sessionsDir, "sessions.json")); const dreamingTranscriptPaths = new Set(); const cronRunTranscriptPaths = new Set(); for (const [sessionKey, entry] of Object.entries(store)) { const transcriptPath = resolveSessionStoreTranscriptPath(sessionsDir, entry); if (!transcriptPath) continue; if (isDreamingNarrativeSessionStoreKey(sessionKey)) dreamingTranscriptPaths.add(transcriptPath); if (isCronRunSessionKey(sessionKey)) cronRunTranscriptPaths.add(transcriptPath); } ... } ``` `isCronRunSessionKey` is `/:cron:[^:]+:run:[^:]+(?::|$)/`. A cron job's *own* session has a key like `agent:main:cron:<jobId>:run:<runId>` and matches. But any **subagent** the cron run spawns (e.g. via `mcp__openclaw__sessions_spawn`, or in my case `lossless-claw`'s LCM retrieval-navigator agents) is recorded with a key of the form `agent:main:subagent:<uuid>` and **never matches**. Those subagents inherit nothing from the parent's cron classification. The downstream consumer in dreaming-phases: ```js // dist/dreaming-phases-nNBnlLlq.js:489-498 if (entry.generatedByDreamingNarrative || entry.generatedByCronRun) { nextFiles[stateKey] = { ... }; ... continue; // ← skip ingestion only when the session's own key matched } ``` So the filter exists, the intent is clear, but the implementation only catches the cron-run session itself — not anything it spawns. ### Local evidence — three polluting transcripts in today's corpus The three transcripts that contributed 75% of today's `session-corpus/2026-06-03.txt` lines all show as `agent:main:subagent:*` in the sessions store, none as cron: ```python target_uuids = [ '33a0a316-2745-46b3-b5c3-8726795b65cc', 'a3a87ce7-16f7-4428-80c8-d6f9cf3d06b2', 'c98df5d4-81ec-4ddf-82ed-826790eb452e', ] # from ~/.openclaw/agents/main/sessions/sessions.json: # 'agent:main:subagent:9f618454-9352-419b-a8b4-e945f738fd74' -> sessionId=a3a87ce7-... # 'agent:main:subagent:fd58d2aa-7963-4ab4-b025-4b692e64b134' -> sessionId=c98df5d4-... # 'agent:main:subagent:e3e481bd-928f-49e2-bf22-6af1ea8b49be' -> sessionId=33a0a316-... # cron-run keys in store: 0 total entries: 29 ``` Every one of those transcripts begins with `User: You are an autonomous LCM retrieval navigator. Plan and execute retrieval before answering. Available tools: lcm_describe, lcm_expand, lcm_grep ...` — the LCM-expansion instruction prompt template, repeated verbatim across hundreds of corpus lines per day. The user message itself contains `originSessionKey: agent:main:cron:603396de-9775-455b-96f6-842f9f0c14bf:run:9955a065-...` — i.e. the parent cron is identified inline in the prompt, just not propagated to the classification store. ## Downstream impact Both defects feed the same surface, and the cumulative effect is that dreaming becomes effectively a no-op: - Each corpus line is staged as its own candidate at the floor `SESSION_INGESTION_SCORE` (0.58) in `memory/.dreams/phase-signals.json`, with `recalls: 0`. - REM's pattern-detection has only single-occurrence boilerplate fragments to cluster on. Today's REM report: `### Reflections — No strong patterns surfaced.` Its `### Possible Lasting Truths` section consists of three Telegram metadata strings (`chat_id`, `sender_id`, `username`) lifted verbatim from corpus lines. - Deep promotes nothing. This matches the symptom shape of #80858 (*"No strong candidate truths surfaced"* placeholder promoted) and #89264 (REM repetitive meta-themes), but the upstream cause is corpus pollution, not the promotion gates. ## Why this is not a duplicate of #72611 #72611 identifies that `generatedByCronRun` classification can fail when a transcript filename is rotated to `.deleted.<timestamp>` because the rotated path no longer matches `sessions.json`'s `sessionFile` field. That is a *path-matching* failure on the cron run's own transcript. This issue is different on both axes: 1. **Defect 1** is the corpus ingester using the wrong file-name predicate (`isUsageCountedSessionTranscriptFileName` instead of `isPrimarySessionTranscriptFileName`), causing **deliberate double-ingestion** of every reset/deleted archive — regardless of whether the path-matching ever succeeds. Even for sessions that classify correctly, the archive variants get fed in as their own additional file. 2. **Defect 2** is a *classification* gap, not a path-matching gap. The subagent transcripts I'm seeing classify correctly as `subagent:*` — they're just not flagged as cron-descended because cron parentage isn't propagated to spawned children. This holds even if path-matching is perfect. Operator-facing config (the bulk of #72611's proposed fix) is still valuable, but won't close either of the two defects above without the underlying predicate / classification changes. ## Suggested fixes 1. **Use `isPrimarySessionTranscriptFileName` in `listSessionFilesForAgent` for dreaming corpus collection** (or introduce a separate `isDreamingIngestibleSessionTranscriptFileName` if usage telemetry needs to remain on `isUsageCountedSessionTranscriptFileName`). One-line change in `engine-qmd-oR1jDIms.js`. 2. **Propagate cron parentage to spawned subagents.** Two viable shapes: - At session creation: when a subagent is spawned from a session whose own key (or transitively, ancestor key) matches `isCronRunSessionKey`, store a `parentTrigger: "cron"` field on the child's `sessions.json` entry, and have `loadSessionTranscriptClassificationForSessionsDir` add the child's transcript to `cronRunTranscriptPaths`. - Alternative: classify after-the-fact by walking parent-session pointers (if `entry` already records `spawnedBy` / `parentSessionKey`). 3. **Defense-in-depth content filter** (overlaps with #80664 and #71656): reject corpus snippets that begin with one of a small set of instruction stems — `"You are an autonomous LCM retrieval navigator"`, `"Return ONLY JSON with this shape"`, etc. Useful even after (1) and (2) land, because it protects against future cron-spawned automation paths the operator hasn't yet thought to classify. ## Environment - `openclaw@2026.6.1` (from `/opt/homebrew/lib/node_modules/openclaw/package.json`) - Memory backend: `memory-core` builtin - Dreaming: `enabled: true`, default phases - Workspace: `~/.openclaw/workspace` - Affected paths: `memory/.dreams/session-corpus/*.txt`, `memory/.dreams/phase-signals.json` ## Related - #67272 (CLOSED, COMPLETED) — original "exclude cron from Dreaming ingestion" feature; the implementation that shipped is what (2) here patches. - #68449 (CLOSED) — same scope; closed alongside #67272. - #72611 (OPEN) — operator-facing exclusion config + rotated-path classification breakage. Adjacent but distinct from this issue's two defects (see "Why this is not a duplicate" above). - #80664 (OPEN) — assistant process-chatter filter. Different content shape (short scratch notes), but the proposed hook point is the same `normalizeSessionCorpusSnippet` boundary that the content-filter half of (3) here would land on. - #71656 (OPEN, stale) — corpus pre-filtering / weighted scoring. - #80858 (OPEN) — `"No strong candidate truths surfaced"` placeholder promotion. Plausibly resolves naturally once the corpus contains real signal. - #89264 (OPEN) — REM repetitive meta-themes / promotion gate bypass. Separate root cause but the same downstream symptom.