Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/cli/src/ui/commands/dreamCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import * as path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import { Storage } from '@qwen-code/qwen-code-core';
import { dreamCommand } from './dreamCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';

describe('dreamCommand', () => {
it('submits a consolidation prompt with the project-scoped transcript directory', async () => {
const projectRoot = path.join('tmp', 'dream-project');
const buildConsolidationPrompt = vi.fn().mockReturnValue('dream prompt');
const writeDreamManualRun = vi.fn();
const context = createMockCommandContext({
services: {
config: {
getProjectRoot: vi.fn().mockReturnValue(projectRoot),
getMemoryManager: vi.fn().mockReturnValue({
buildConsolidationPrompt,
writeDreamManualRun,
}),
getSessionId: vi.fn().mockReturnValue('session-1'),
},
},
});

const result = await dreamCommand.action?.(context, '');
const expectedTranscriptDir = path.join(
new Storage(projectRoot).getProjectDir(),
'chats',
);

expect(result).toEqual({
type: 'submit_prompt',
content: 'dream prompt',
onComplete: expect.any(Function),
});
expect(buildConsolidationPrompt).toHaveBeenCalledWith(
expect.any(String),
expectedTranscriptDir,
);
expect(expectedTranscriptDir).not.toContain(
`${path.sep}.qwen${path.sep}tmp${path.sep}`,
);
});
});
13 changes: 6 additions & 7 deletions packages/cli/src/ui/commands/dreamCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {
getAutoMemoryRoot,
getProjectHash,
QWEN_DIR,
} from '@qwen-code/qwen-code-core';
import * as path from 'node:path';
import { getAutoMemoryRoot, Storage } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js';
Expand All @@ -31,8 +28,10 @@ export const dreamCommand: SlashCommand = {

const projectRoot = config.getProjectRoot();
const memoryRoot = getAutoMemoryRoot(projectRoot);
const projectHash = getProjectHash(projectRoot);
const transcriptDir = `${QWEN_DIR}/tmp/${projectHash}/chats`;
const transcriptDir = path.join(
new Storage(projectRoot).getProjectDir(),
'chats',
);

const prompt = config
.getMemoryManager()
Expand Down
49 changes: 48 additions & 1 deletion packages/core/src/memory/dreamAgentPlanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Config } from '../config/config.js';
import { Storage } from '../config/storage.js';
import type { ForkedAgentResult } from '../utils/forkedAgent.js';
import { runForkedAgent } from '../utils/forkedAgent.js';
import { planManagedAutoMemoryDreamByAgent } from './dreamAgentPlanner.js';
import { escapeShellArg, getShellConfiguration } from '../utils/shell-utils.js';
import {
buildConsolidationTaskPrompt,
getTranscriptDir,
planManagedAutoMemoryDreamByAgent,
} from './dreamAgentPlanner.js';
import { ensureAutoMemoryScaffold } from './store.js';

vi.mock('../utils/forkedAgent.js', () => ({
Expand All @@ -38,6 +44,7 @@ describe('dreamAgentPlanner', () => {
});

afterEach(async () => {
Storage.setRuntimeBaseDir(null);
await fs.rm(tempDir, {
recursive: true,
force: true,
Expand All @@ -46,6 +53,46 @@ describe('dreamAgentPlanner', () => {
});
});

it('returns project-scoped session transcript directory', () => {
const runtimeDir = path.join(tempDir, 'runtime');
Storage.setRuntimeBaseDir(runtimeDir);

expect(getTranscriptDir(projectRoot)).toBe(
path.join(new Storage(projectRoot).getProjectDir(), 'chats'),
);
expect(getTranscriptDir(projectRoot)).toContain(
path.join(runtimeDir, 'projects'),
);
expect(getTranscriptDir(projectRoot)).not.toContain(
`${path.sep}.qwen${path.sep}tmp${path.sep}`,
);
});

it('shell-quotes the transcript directory in the grep example', () => {
const transcriptDir = path.join(
tempDir,
'runtime dir; touch BAD',
'projects',
'-tmp-project',
'chats',
);
const quotedTranscriptDir = escapeShellArg(
`${transcriptDir}${path.sep}`,
getShellConfiguration().shell,
);
const prompt = buildConsolidationTaskPrompt(
path.join(tempDir, 'memory'),
transcriptDir,
);

expect(prompt).toContain(
`grep -rn "<narrow term>" ${quotedTranscriptDir} --include="*.jsonl" | tail -50`,
);
expect(prompt).not.toContain(
`grep -rn "<narrow term>" ${transcriptDir}${path.sep} --include="*.jsonl" | tail -50`,
);
});

it('returns the forked agent result', async () => {
const mockResult: ForkedAgentResult = {
status: 'completed',
Expand Down
23 changes: 17 additions & 6 deletions packages/core/src/memory/dreamAgentPlanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
runForkedAgent,
type ForkedAgentResult,
} from '../utils/forkedAgent.js';
import { getProjectHash, QWEN_DIR } from '../utils/paths.js';
import * as path from 'node:path';
import { Storage } from '../config/storage.js';
import {
AUTO_MEMORY_INDEX_FILENAME,
getAutoMemoryRoot,
Expand All @@ -22,7 +23,11 @@ import type {
PermissionDecision,
} from '../permissions/types.js';
import { isShellCommandReadOnlyAST } from '../utils/shellAstParser.js';
import { stripShellWrapper } from '../utils/shell-utils.js';
import {
escapeShellArg,
getShellConfiguration,
stripShellWrapper,
} from '../utils/shell-utils.js';

const MAX_TURNS = 8;
const MAX_TIME_MINUTES = 5;
Expand Down Expand Up @@ -160,15 +165,21 @@ Rules:
- Keep the MEMORY.md index concise: one line per file in the format \`- [Title](relative/path.md) — one-line hook\`.
- If nothing needs consolidation, do nothing and say so.`;

function getTranscriptDir(projectRoot: string): string {
const projectHash = getProjectHash(projectRoot);
return `${QWEN_DIR}/tmp/${projectHash}/chats`;
export function getTranscriptDir(projectRoot: string): string {
return path.join(new Storage(projectRoot).getProjectDir(), 'chats');
Comment thread
LaZzyMan marked this conversation as resolved.
}

function quoteShellPathWithTrailingSeparator(dirPath: string): string {
return escapeShellArg(`${dirPath}${path.sep}`, getShellConfiguration().shell);
}

export function buildConsolidationTaskPrompt(
memoryRoot: string,
transcriptDir: string,
): string {
const quotedTranscriptDir =
quoteShellPathWithTrailingSeparator(transcriptDir);

return [
`Memory directory: \`${memoryRoot}\``,
'This directory already exists — write to it directly with the write_file tool (do not run mkdir or check for its existence).',
Expand All @@ -187,7 +198,7 @@ export function buildConsolidationTaskPrompt(
'',
'1. Existing memories that drifted — facts that contradict something you now know from current memory files',
'2. Transcript search — if you need specific context, grep session transcripts for narrow terms:',
` \`grep -rn "<narrow term>" ${transcriptDir}/ --include="*.jsonl" | tail -50\``,
` \`grep -rn "<narrow term>" ${quotedTranscriptDir} --include="*.jsonl" | tail -50\``,
'',
"Don't exhaustively read transcripts. Look only for things you already suspect matter.",
'',
Expand Down
Loading