Skip to content

fix(context): inject project dream namespaces alongside recent raw memory (closes #2834)#2858

Open
rodboev wants to merge 10 commits into
thedotmack:mainfrom
rodboev:fix/2834-dream-context-injection
Open

fix(context): inject project dream namespaces alongside recent raw memory (closes #2834)#2858
rodboev wants to merge 10 commits into
thedotmack:mainfrom
rodboev:fix/2834-dream-context-injection

Conversation

@rodboev

@rodboev rodboev commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Distilled memories stored under <project>:dream were absent from normal project context injection because the default query path only used the cwd-derived raw project name. This PR adds the dream namespace to the default project list and keeps a bounded recent raw-memory fallback so distilled context becomes primary without hiding fresh undistilled observations.

Why

generateContext() in src/services/context/ContextBuilder.ts:105-128 already supports multi-project queries, and queryObservationsMulti() / querySummariesMulti() in src/services/context/ObservationCompiler.ts:88-175 already union rows across project lists. The gap was that getProjectContext() in src/utils/project-name.ts:76-96 never emitted the companion dream namespace for ordinary project sessions, so the multi-project path was not used for distilled memory at all.

Scope

This PR is intentionally read-path only. It does not change how dream rows are written, does not migrate stored project names, and does not inject any unscoped global dream namespace.

Risk

The main risk is over-weighting old distilled rows and crowding out fresh raw context. The fix keeps that bounded by preserving a recent raw fallback and by leaving the rendered project label unchanged even though the underlying query list expands.

Verification / Test plan

  • bun test tests/utils/project-name.test.ts — 18 passed, 2 failed on the pre-existing Windows ~ expectations that currently expect C:\Users\Rod instead of the actual basename Rod; the new dream-namespace assertions are green.
  • bun test tests/hooks/file-context.test.ts — 9 passed; file-context requests now carry both the dream namespace and the raw project name.
  • bun test tests/worker/http/routes/search-routes-welcome-hint.test.ts — 4 passed; existing multi-project query handling is unchanged.
  • npm run build — passed; regenerated plugin/scripts/context-generator.cjs and plugin/scripts/transcript-watcher.cjs from the shared project-context/query changes.
  • npm run lint:hook-io && npm run lint:spawn-env — passed.
  • npm run strip-comments:check — fails on the existing repo-wide baseline (Changed: 301 in check mode), unrelated to this branch.

Closes #2834

@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a gap where <project>:dream distilled-memory namespaces were never included in the project query list, so dream rows were silently absent from every context injection. It wires the dream namespace into getProjectContext's allProjects, adds prioritizeProjectRows to surface dream rows first while guaranteeing at least one recent raw row, and introduces queryObservationsMulti/querySummariesMulti to union rows across the expanded project list.

  • src/utils/project-name.ts: withDreamProject now emits [parent:dream, worktree:dream, parent, worktree] for worktree sessions and [proj:dream, proj] for normal sessions; the null-cwd path intentionally stays dream-free.
  • src/services/context/ObservationCompiler.ts: prioritizeProjectRows places dream rows first and caps preferred rows at limit - 1, guaranteeing at least one raw fallback slot when raw rows exist and limit > 1.
  • src/services/context/ContextBuilder.ts: Because allProjects always has ≥ 2 entries, generateContext now always routes through the multi-project path, making dream rows visible in every context injection.

Confidence Score: 4/5

Safe to merge for the primary feature, but users with CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE=true will silently lose the show-last-message capability once any dream row exists.

The read-path expansion from queryObservations to queryObservationsMulti works correctly for context injection, and prioritizeProjectRows handles the raw-row guarantee. The one concrete regression is in getPriorSessionMessages: because the prioritized observation list puts dream rows first, find short-circuits on the first dream row (whose session ID is always different from the current user session), constructs a transcript path that does not exist, and returns empty strings — skipping every raw-observation transcript. This silently breaks the show last message feature for any user who has enabled it. Since that setting defaults to false the blast radius is limited, but the regression is real for opted-in users.

src/services/context/ObservationCompiler.ts (getPriorSessionMessages) and src/services/context/ContextBuilder.ts (mostRecentObservation = observations[0]) both need to account for dream rows appearing at the top of the prioritized list.

Important Files Changed

Filename Overview
src/utils/project-name.ts Adds getDreamProjectName, withDreamProject, and updates getProjectContext to include dream namespaces in allProjects for both normal and worktree sessions. Null-cwd path intentionally omits the dream namespace. Logic is correct; the worktree composite dream namespace is now included.
src/services/context/ObservationCompiler.ts Adds prioritizeProjectRows, queryObservationsMulti, querySummariesMulti, and countObservationsByProjects. The prioritization logic correctly guarantees at least 1 raw row when limit > 1. However, placing dream rows first breaks getPriorSessionMessages, which short-circuits at the dreaming-session row whose transcript does not exist at the user-cwd path.
src/services/context/ContextBuilder.ts The branch on projects.length > 1 now always routes through the multi-project path since allProjects always contains at least 2 entries. The mostRecentObservation = observations[0] is now a dream row when distilled memory exists, which may affect shouldShowSummary and the show-last-message feature.
tests/context/observation-compiler.test.ts New prioritizeProjectRows tests cover both the single-dream-project case and the worktree multi-dream case. No test covers getPriorSessionMessages behaviour when dream rows precede raw rows.
tests/utils/project-name.test.ts Adds dream-namespace assertions for normal paths, worktree paths, and the null-cwd edge case. The worktree test validates that both parent:dream and composite:dream are included.
tests/hooks/file-context.test.ts Updates the getProjectContext mock to return dream+raw allProjects and asserts the projects query param equals test-project:dream,test-project.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Hook fires: getProjectContext(cwd)"] --> B{cwd null?}
    B -- yes --> C["allProjects = [rawProject]"]
    B -- no --> D{isWorktree?}
    D -- yes --> E["allProjects = [parent:dream, composite:dream, parent, composite]"]
    D -- no --> F["allProjects = [proj:dream, proj]"]
    C --> G[generateContext]
    E --> G
    F --> G
    G --> H{"projects.length > 1?"}
    H -- "always yes" --> I["queryObservationsMulti + querySummariesMulti"]
    I --> K[prioritizeProjectRows]
    K --> L{"dream rows exist?"}
    L -- yes --> M["emit limit-1 dream rows + at least 1 raw row"]
    L -- no --> N["emit limit raw rows"]
    M --> O[buildContextOutput]
    N --> O
    O --> P["getPriorSessionMessages: find first obs != currentSession"]
    P --> R["dream row found first, transcript missing, lastMessage empty"]
Loading

Comments Outside Diff (1)

  1. src/services/context/ObservationCompiler.ts, line 280-289 (link)

    P1 getPriorSessionMessages always binds to the dreaming session, silently breaking "show last message"

    prioritizeProjectRows places dream rows at the front of observations. Because the dreaming process runs as a background agent, its memory_session_id is never equal to the current user session's ID. This means observations.find(obs => obs.memory_session_id !== currentSessionId) immediately returns the first dream row. The function then constructs transcriptPath for the dreaming session under the user's cwd, which doesn't exist, so extractPriorMessages silently returns empty strings. Every raw-observation transcript is skipped.

    For any user who has CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE=true, this regresses to "no last message ever shown" as soon as the first dream row is ingested — with no error or log line to indicate why.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/services/context/ObservationCompiler.ts
    Line: 280-289
    
    Comment:
    **`getPriorSessionMessages` always binds to the dreaming session, silently breaking "show last message"**
    
    `prioritizeProjectRows` places dream rows at the front of `observations`. Because the dreaming process runs as a background agent, its `memory_session_id` is never equal to the current user session's ID. This means `observations.find(obs => obs.memory_session_id !== currentSessionId)` immediately returns the first dream row. The function then constructs `transcriptPath` for the dreaming session under the user's `cwd`, which doesn't exist, so `extractPriorMessages` silently returns empty strings. Every raw-observation transcript is skipped.
    
    For any user who has `CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE=true`, this regresses to "no last message ever shown" as soon as the first dream row is ingested — with no error or log line to indicate why.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/services/context/ObservationCompiler.ts:280-289
**`getPriorSessionMessages` always binds to the dreaming session, silently breaking "show last message"**

`prioritizeProjectRows` places dream rows at the front of `observations`. Because the dreaming process runs as a background agent, its `memory_session_id` is never equal to the current user session's ID. This means `observations.find(obs => obs.memory_session_id !== currentSessionId)` immediately returns the first dream row. The function then constructs `transcriptPath` for the dreaming session under the user's `cwd`, which doesn't exist, so `extractPriorMessages` silently returns empty strings. Every raw-observation transcript is skipped.

For any user who has `CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE=true`, this regresses to "no last message ever shown" as soon as the first dream row is ingested — with no error or log line to indicate why.

Reviews (5): Last reviewed commit: "Keep parser mode tests from leaking acro..." | Re-trigger Greptile

Comment thread src/services/context/ObservationCompiler.ts Outdated
@rodboev

rodboev commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Added the worktree composite dream namespace to allProjects, enforced a raw-row fallback slot when dream rows fill the limit, and covered both behaviors in focused tests. The previous red CI on this branch matched the shared baseline now isolated in #2853, this push is the branch-local Greptile follow-up.

@thedotmack

Copy link
Copy Markdown
Owner

Review finding that blocks this one: prioritizeProjectRows breaks recency-ordering assumptions downstream. It returns [dream-rows..., raw-rows...], but three consumers assume index 0 is the most recent row: getPriorSessionMessages() takes element 0's session as "the prior session" (would resolve to an old dream-distillation session), prepareSummariesForTimeline() uses allSummaries[i+1] as the older summary for displayEpoch, and buildContextOutput's summaries[0]/observations[0] semantics shift. buildTimeline re-sorts, but those three don't. Second concern: nothing in the tree writes a :dream project namespace (zero grep hits across src/, openclaw/, scripts/), so this adds a read path — plus an inflated SQL LIMIT on every normal session — for a producer that doesn't exist in-repo yet.\n\nIf the dream namespace is coming from an external workflow, suggest: interleave by epoch instead of prepending (preserve sort order), and gate the multi-project query path behind the namespace actually having rows. Happy to merge a revision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix distilled memory context injection for project dream namespaces

2 participants