Skip to content

feat(memory): managed auto-memory and auto-dream system#3087

Merged
tanzhenxin merged 61 commits into
QwenLM:mainfrom
LaZzyMan:feat/auto-memory
Apr 16, 2026
Merged

feat(memory): managed auto-memory and auto-dream system#3087
tanzhenxin merged 61 commits into
QwenLM:mainfrom
LaZzyMan:feat/auto-memory

Conversation

@LaZzyMan

@LaZzyMan LaZzyMan commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR aligns Qwen Code's extract and dream memory subsystems to Claude Code's implementation patterns, and fixes a critical bug that prevented extract from ever triggering in normal usage.


Bug Fix

saveCacheSafeParams never called in skipNextSpeakerCheck path

client.ts had saveCacheSafeParams placed at the bottom of sendMessageStream, after all early-return statements. Since skipNextSpeakerCheck defaults to true, the early-return branch was always taken, meaning saveCacheSafeParams was never called. The extract agent's first action is getCacheSafeParams(), which returned null and threw — so extract never ran in practice.

Fix: move saveCacheSafeParams to the top of the no-pending-tool-calls block, before any branch exits.


Alignment Changes

extract — tool constraints

  • Added run_shell_command to the allowed tool list (read-only commands only)
  • Added inline createMemoryScopedAgentConfig() with a PermissionManager wrapper that:
    • Allows only AST-verified read-only shell commands (isShellCommandReadOnlyAST)
    • Restricts write_file/edit to paths inside the auto-memory directory (isAutoMemPath)
    • Implements isToolEnabled() at the registry level to prevent CoreToolScheduler TypeError crash

extract — prompt design

  • Added memory manifest block listing existing files with topic/path/current-content preview
  • Introduced parallel read-then-write strategy to stay within turn budget
  • Clarified two-step save: Step 1 = write memory file, Step 2 = update index

dream — remove mechanical fallback

  • Deleted buildDreamedBody(), writeUpdatedBody(), and all mechanical dedup/merge code paths
  • runManagedAutoMemoryDream is now agent-only; throws without config instead of silently falling back

dream — prompt design

  • Restructured to Claude Code's 4-phase prompt:
    • Phase 1 Orient: scan memory dir including logs//sessions/ subdirs
    • Phase 2 Gather: narrow transcript grep with two signal-source priority rules
    • Phase 3 Consolidate: merge + relative-to-absolute date conversion
    • Phase 4 Prune & Index: stale/wrong file removal, index size cap (~200 lines / ~25 KB)

Test Updates

  • extractionAgentPlanner.test.ts: updated expected tools array (added run_shell_command)
  • dreamAgentPlanner.test.ts: updated expected tools array
  • dream.test.ts: removed mechanical-dedup tests; added agent-only tests (throws without config, propagates planner result, propagates planner failures)
  • dreamScheduler.test.ts: mocked dream.js so scheduler logic tests are independent; replaced removed mechanical-dedup test with scheduler infrastructure verification (task metadata propagation, lock release, session ID write)

#1267
#2991

LaZzyMan added 30 commits April 1, 2026 11:28
Feature 3 - Memory Saved Notification:
- Add HistoryItemMemorySaved type to types.ts
- Create MemorySavedMessage component for rendering '● Saved/Updated N memories'
- In useGeminiStream: detect in-turn memory writes via mapToDisplay's
  memoryWriteCount field and emit 'memory_saved' history item after turn
- In client.ts: capture background dream/extract promises and expose
  via consumePendingMemoryTaskPromises(); useGeminiStream listens
  post-turn and emits 'Updated N memories' notification for background tasks

Feature 4 - Memory Count Badge:
- Add isMemoryOp field to IndividualToolCallDisplay
- Add memoryWriteCount/memoryReadCount to HistoryItemToolGroup
- Add detectMemoryOp() in useReactToolScheduler using isAutoMemPath
- ToolGroupMessage renders '● Recalled N memories, Wrote N memories' badge
  at the top of tool groups that touch memory files

Fix: process.env bracket-access in paths.ts (noPropertyAccessFromIndexSignature)
Fix: MemoryDialog.test.tsx mock useSettings to satisfy SettingsProvider requirement
…ps, fix MEMORY.md path

Problem 1 - Auto-approve memory file operations:
- write-file.ts: getDefaultPermission() checks isAutoMemPath; returns 'allow'
  for managed auto-memory files, 'ask' for all other files
- edit.ts: same pattern

Problem 2 - Feature 4 UX: collapse memory-only tool groups:
- ToolGroupMessage: detect when all tool calls have isMemoryOp set (pure memory
  group) and all are complete; render compact '● Recalled/Wrote N memories
  (ctrl+o to expand)' instead of individual tool call rows
- ctrl+o toggles expand/collapse when isFocused and group is memory-only
- Mixed groups (memory + other tools) keep badge-at-top behaviour
- Expanded state shows individual tool calls with '● Memory operations
  (ctrl+o to collapse)' header

Problem 3 - MEMORY.md path mismatch:
- prompt.ts: Step 2 now references full absolute path ${memoryDir}/MEMORY.md
  so the model writes to the correct location inside the memory directory,
  not to the parent project directory

Fix tests:
- write-file.test.ts: add getProjectRoot to mockConfigInternal
- prompt.test.ts: update assertion to match full-path section header
…ool detection

- Remove duplicate 'Saved N memories' notification: the tool group badge already
  shows 'Wrote N memories'; the separate HistoryItemMemorySaved addItem after
  onComplete was double-counting. Keep only the background-task path
  (consumePendingMemoryTaskPromises).

- Remove ctrl+o expand: Ink's Static area freezes items on first render and
  cannot respond to user input. useInput/useState(isExpanded) in a Static item
  is a no-op. Removed the dead code; memory-only groups now always render as
  the compact summary (no fake interactive hint).

- Fix Edit tool detection: detectMemoryOp was checking for 'edit_file' but the
  real tool name constant is 'edit'. Also removed non-existent 'create_file'
  (write_file covers all writes). Now editing MEMORY.md is correctly identified
  as a memory write op, collapses to 'Wrote N memories', and is auto-approved.
…background agent

The previous implementation ran an AgentHeadless background agent that could
take 5+ minutes with zero UI feedback — user saw a blank screen for the entire
duration and then at most one line of text.

Fix: /dream now returns submit_prompt with the consolidation task prompt so it
runs as a regular AI conversation turn. Tool calls (read_file, write_file, edit,
grep_search, list_directory, glob) are immediately visible as collapsed tool
groups as the model works through the memory files — identical UX to Claude Code.

Also export buildConsolidationTaskPrompt from dreamAgentPlanner so dreamCommand
can reuse the same detailed consolidation prompt that was already written.
Add getMemoryBaseDir() to getDefaultPermission() allow list in ls.ts,
glob.ts, and grep.ts — mirrors the existing pattern in read-file.ts.

Without this, ListFiles/Glob/Grep on ~/.qwen/* would trigger an
approval dialog, blocking /dream at its very first step.
Match Claude Code's headless-agent intent: background memory agents must never
block on interactive permission prompts.

Wrap background runtime config so getApprovalMode() returns YOLO, ensuring any
ask decision is auto-approved instead of hanging forever. Add regression test
covering the wrapped approval mode.
Make managed auto-memory extraction follow the Claude Code architecture:
background extraction now uses a forked agent to read/write memory files
directly, instead of planning patches and applying them with a separate
filesystem pipeline.

Keep the old patch/model path only as fallback if the forked agent fails.
Add regression tests covering the new execution path and tool whitelist.
Delete the old patch/model/heuristic extraction path entirely.
Managed auto-memory extract now runs only through the forked-agent
execution flow, with no planner/apply fallback stages remaining.

Also remove obsolete exports/tests and update scheduler/integration
coverage to use the forked-agent-only architecture.
meta.json, extract-cursor.json, and consolidation.lock are internal
bookkeeping files, not user-visible memories. Move them one level up
to the project state dir (parent of memory/) so that the memory/
directory contains only MEMORY.md and topic files, matching the
clean layout of the upstream reference implementation.

Add getAutoMemoryProjectStateDir() helper in paths.ts and update the
three path accessors + store.test.ts path assertions accordingly.
…anup

- Merge runForkedQuery into runForkedAgent via TypeScript overloads:
  with cacheSafeParams → GeminiChat single-turn path (ForkedQueryResult)
  without cacheSafeParams → AgentHeadless multi-turn path (ForkedAgentResult)
- Delete forkedQuery.ts; move its test to background/forkedAgent.cache.test.ts
- Remove forkedQuery export from followup/index.ts
- Migrate all callers (suggestionGenerator, speculation, btwCommand, client)
  to import from background/forkedAgent
- Add getFastModel() / setFastModel() to Config; expose in CLI config init
  and ModelDialog / modelCommand
- Remove resolveFastModel() from AppContainer — now delegated to config.getFastModel()
- Strip Claude Code references from code comments
- dream.ts: writeDreamManualRunToMetadata now persists lastDreamSessionId
  and resets recentSessionIdsSinceDream, preventing auto-dream from firing
  again in the same session after a manual /dream
- config.ts: gate managed auto-memory injection on getManagedAutoMemoryEnabled();
  when disabled, previously saved memories are no longer injected into new sessions
- rememberCommand.ts: remove legacy save_memory branch (tool was removed);
  fall back to submit_prompt directing agent to write to QWEN.md instead
- BuiltinCommandLoader.ts: only register /dream and /forget when managed
  auto-memory is enabled, matching the feature's runtime availability
- forget.ts: return early in forgetManagedAutoMemoryMatches when matches is
  empty, avoiding unnecessary directory scaffolding as a side effect
- fix(client): move saveCacheSafeParams before early-return paths so
  extract agents always have cache params available (fixes extract never
  triggering in skipNextSpeakerCheck mode)

- feat(extract): add read-only shell tool + memory-scoped write
  permissions; create inline createMemoryScopedAgentConfig() with
  PermissionManager wrapper (isToolEnabled + evaluate) that allows only
  read-only shell commands and write/edit within the auto-memory dir

- feat(extract): align prompt to Claude Code patterns — manifest block
  listing existing files, parallel read-then-write strategy, two-step
  save (memory file then index)

- feat(dream): remove mechanical fallback; runManagedAutoMemoryDream is
  now agent-only and throws without config

- feat(dream): align prompt to Claude Code 4-phase structure
  (Orient/Gather/Consolidate/Prune+Index); add narrow transcript grep,
  relative→absolute date conversion, stale index pruning, index size cap

- fix(permissions): add isToolEnabled() to MemoryScopedPermissionManager
  to prevent TypeError crash in CoreToolScheduler._schedule

- test: update dreamScheduler tests to mock dream.js; replace removed
  mechanical-dedup test with scheduler infrastructure verification
@LaZzyMan LaZzyMan requested a review from wenshao April 14, 2026 09:06
… MemoryBackgroundTaskHub

- Add memoryTaskHub.ts: single BackgroundTaskRegistry + BackgroundTaskDrainer shared
  by all memory background tasks; exposes listExtractTasks() / listDreamTasks()
  typed query helpers and a unified drain() method
- extractScheduler: ManagedAutoMemoryExtractRuntime accepts hub via constructor
  (defaults to defaultMemoryTaskHub); test factory gets isolated fresh hub
- dreamScheduler: same pattern — sessionScanner + hub injection; BackgroundTask-
  Scheduler initialized from injected hub; test factory gets isolated hub
- status.ts: replace two separate getRegistry() calls with defaultMemoryTaskHub
  typed query methods
- Footer.tsx (useDreamRunning): subscribe to shared registry, filter by
  DREAM_TASK_TYPE so extract tasks do not trigger the dream spinner
- index.ts: re-export memoryTaskHub.ts so defaultMemoryTaskHub/DREAM_TASK_TYPE/
  EXTRACT_TASK_TYPE are available as top-level package exports
Replace memory-specific MemoryBackgroundTaskHub with a domain-agnostic
BackgroundTaskHub in the background/ layer. Any future background task
runtime (3rd, 4th, …) plugs in by accepting a hub via constructor
injection — no new infrastructure required.

Changes:
- Add background/taskHub.ts: BackgroundTaskHub (registry + drainer +
  createScheduler() + listByType(taskType, projectRoot?)) and the
  globalBackgroundTaskHub singleton. Zero knowledge of any task type.
- Delete memory/memoryTaskHub.ts: its narrow listExtractTasks /
  listDreamTasks helpers are replaced by the generic listByType() call.
- Move EXTRACT_TASK_TYPE to extractScheduler.ts (owned by the runtime
  that defines it); replace 3 hardcoded string literals with the const.
- Move DREAM_TASK_TYPE to dreamScheduler.ts; use hub.createScheduler()
  instead of manually wiring new BackgroundTaskScheduler(reg, drain).
- status.ts: globalBackgroundTaskHub.listByType(EXTRACT_TASK_TYPE, ...)
- Footer.tsx: globalBackgroundTaskHub.registry (shared, filtered by type)
- index.ts: export background/taskHub.js; drop memory/memoryTaskHub.js
const docEntries = parseAutoMemoryEntries(doc.body);
for (const entry of docEntries) {
entries.push({
id: doc.relativePath,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Every parsed entry from the same multi-entry document gets the same entryId (doc.relativePath). A few lines later, entryById is built as a Map keyed by that ID, so siblings collapse to the last entry and the model's governance response can be applied to the wrong memory item. Please switch to a stable per-entry ID (for example relativePath:index) and build entryById from those unique IDs.

— gpt-5.4 via Qwen Code /review


function getTranscriptDir(projectRoot: string): string {
const projectHash = getProjectHash(projectRoot);
return `${QWEN_DIR}/tmp/${projectHash}/chats`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The dream agent is pointed at ${QWEN_DIR}/tmp/${projectHash}/chats, but chat/session persistence now uses Storage.getProjectDir()/chats via SessionService / ChatRecordingService. In practice the transcript grep will usually search the wrong directory and miss recent session signal, which breaks the consolidation logic that depends on transcript context. Please derive this path from the same storage API as the session writers instead of reconstructing it here.

— gpt-5.4 via Qwen Code /review

// tool was removed.
return {
type: 'submit_prompt',
content: `Please save the following fact to memory (e.g. append to QWEN.md in the project root):\n\n${fact}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] In the non-managed path /remember now asks the agent to update QWEN.md through normal file tools instead of save_memory. However, the live refresh path in useGeminiStream still only runs after successful save_memory calls, so this write reaches disk but does not become visible to the current session until restart/refresh. Please trigger memory refresh when completed tool batches modify hierarchical memory files as well, not only when save_memory succeeds.

— gpt-5.4 via Qwen Code /review

projectRoot: string,
): Promise<ScannedAutoMemoryDocument[]> {
const root = getAutoMemoryRoot(projectRoot);
const relativePaths = await listMarkdownFiles(root);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] scanAutoMemoryTopicDocuments() loads and stats every markdown file before sorting and applying the 200-file cap. Because this helper is reused by recall, status, forget, and index rebuild flows, hot paths will scale with the full memory tree size instead of the capped working set. Consider capping before reading file bodies and/or reusing a cached incremental scan result.

— gpt-5.4 via Qwen Code /review


const handleToggleAutoMemory = useCallback(() => {
const newValue = !autoMemoryOn;
loadedSettings.setValue(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This toggle persists the workspace setting and updates local checkbox state, but built-in command availability is decided when commands are loaded from config in BuiltinCommandLoader. That means the dialog can show managed auto-memory as enabled/disabled while /dream and /forget still reflect the previous mode until a later reload/restart. Please either reload the effective config/command set after toggling or make the restart requirement explicit in the UI.

— gpt-5.4 via Qwen Code /review

…checks

- background/taskHub.test.ts (11 tests):
  - createScheduler(): tasks registered via scheduler appear in hub registry;
    multiple calls return distinct scheduler instances
  - listByType(): filters by taskType, filters by projectRoot, returns []
    for unknown types, two types co-exist in registry but stay separated
  - drain(): resolves false on timeout, resolves true when tasks complete,
    resolves true immediately when no tasks in flight
  - isolation: tasks in hubA do not appear in hubB
  - globalBackgroundTaskHub: is a BackgroundTaskHub instance with registry/drainer

- extractScheduler.test.ts (+1 test):
  - factory-created runtimes have isolated registries; tasks in runtimeA
    are invisible to runtimeB; all tasks carry EXTRACT_TASK_TYPE

- dreamScheduler.test.ts (+1 test):
  - factory-created runtimes have isolated registries; tasks in runtimeA
    are invisible to runtimeB; all tasks carry DREAM_TASK_TYPE
Replace BackgroundTaskRegistry/Drainer/Scheduler/Hub helper classes and
module-level globals with a single MemoryManager class owned by Config.

## Changes

### New
- packages/core/src/memory/manager.ts — MemoryManager with:
  - scheduleExtract / scheduleDream (inline queuing + deduplication logic)
  - recall / forget / selectForgetCandidates / forgetMatches
  - getStatus / drain / appendToUserMemory
  - subscribe(listener) compatible with useSyncExternalStore
  - storeWith() atomic record registration (no double-notify)
  - Distinct skippedReason 'scan_throttled' vs 'min_sessions' for dream
- packages/core/src/utils/forkedAgent.ts — pure cache util (moved from background/)
- packages/core/src/utils/sideQuery.ts — pure util (moved from auxiliary/)

### Deleted
- background/taskRegistry, taskDrainer, taskScheduler, taskHub and all tests
- background/forkedAgent (moved to utils/)
- auxiliary/sideQuery (moved to utils/)
- memory/extractScheduler, dreamScheduler, state and all tests

### Modified
- config/config.ts — Config owns MemoryManager instance; getMemoryManager()
- core/client.ts — all memory ops via config.getMemoryManager()
- core/client.test.ts — mock MemoryManager instead of individual modules
- memory/status.ts — accepts MemoryManager param, drops globalBackgroundTaskHub
- index.ts — memory exports reduced from 14 modules to 5 (manager/types/paths/store/const)
- cli/commands/dreamCommand.ts — via config.getMemoryManager()
- cli/commands/forgetCommand.ts — via config.getMemoryManager()
- cli/components/Footer.tsx — useSyncExternalStore replacing setInterval polling
- cli/components/Footer.test.tsx — add getMemoryManager mock

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tanzhenxin tanzhenxin dismissed wenshao’s stale review April 16, 2026 12:05

Remaining items can be addressed in follow-up PRs

@tanzhenxin tanzhenxin merged commit 9e2f63a into QwenLM:main Apr 16, 2026
14 checks passed
wenshao added a commit that referenced this pull request Apr 17, 2026
Conflicts in Footer.tsx, Footer.test.tsx, useStatusLine.ts — all from
main's auto-memory/dream feature (#3087) and statusline sync-exec crash
fix (#3264) landing on the same lines as our multi-line refactor.

Resolutions:
- Footer.tsx: keep our `lines` API, add main's `dreamRunning` hook
- Footer.test.tsx: keep both mocks (useStatusLine + core's dream registry)
- useStatusLine.ts: wrap our filter-and-cap logic in main's try/catch
  around exec(); convert main's `setOutput(null)` on sync-throw to
  `setOutput([])` for the new array API
- useStatusLine.test.ts: convert main's new spawn-failure tests from
  `.text` to `.lines`
tanzhenxin added a commit that referenced this pull request Apr 27, 2026
…Dir()

The auto-memory subsystem (introduced on main in #3087) computed its base
directory by hardcoding path.join(os.homedir(), QWEN_DIR). That bypassed
QWEN_HOME entirely, so global auto-memory artifacts always landed in
~/.qwen/projects/... regardless of the user's configured QWEN_HOME path.

Route the default through Storage.getGlobalQwenDir() so QWEN_HOME is
honored. The QWEN_CODE_MEMORY_BASE_DIR test override stays as the
highest-priority short-circuit.

Discovered while running the QWEN_HOME e2e test plan against the merged
branch — Group B test B3 (memory tool writes to QWEN_HOME) was the only
failing scenario across A/B/C/D groups.
mabry1985 added a commit to protoLabsAI/protoCLI that referenced this pull request May 3, 2026
…LM#3717) (#211)

Cherry-picked from QwenLM/qwen-code: 6efcf2b

Adds a session-scoped FileReadCache that lets ReadFile substitute a
short placeholder for full text Reads of files the model has already
seen end-to-end and that have not been modified since. Range-scoped
Reads, non-text payloads, truncated reads, and post-write Reads keep
going through the full pipeline.

Compaction interaction is handled by upstream's own client.ts hook:
when chat compaction succeeds, getFileReadCache().clear() fires so
post-compaction Reads re-emit bytes the model can no longer retrieve
from its truncated context.

The cache is keyed by (stats.dev, stats.ino) so symlinks, hardlinks,
and case-variant paths converge to one entry; rm + recreate is
correctly identified as a fresh entry. The escape hatch
Config.fileReadCacheDisabled flag (default false) lets operators
fully disable the fast-path.

Adaptations from upstream:

- Dropped the auto-memory isAutoMemPath / memoryFreshnessNote
  imports — both come from the un-ported QwenLM#3087 managed-memory
  subsystem. The cache treats every text file uniformly; if we ever
  port the auto-memory branch we'll re-introduce the bypass for
  AGENTS.md-style files.
- Dropped the BackgroundTaskRegistry / BackgroundShellRegistry
  imports/fields the cherry-pick tried to add to Config — those
  belong to the un-ported background-agents subsystem.
- Kept our existing trackFileRead (read-before-edit enforcement)
  and sessionFileTracker.record (P3 external-change detection)
  alongside upstream's new cache.recordRead — they're orthogonal
  and all run in the post-read recording block.
- Dropped the params.pages === undefined arm of isFullRead; we
  haven't ported the PDF/Jupyter pages parameter yet (QwenLM#3160).
  Detection on offset+limit covers our case.

Tests: 163 across the four touched test files (29 for the cache
service itself; 9 for read-file caching paths; new write-file
recordWrite test; new edit.ts FileReadCache integration test).
typecheck + core build clean.

Used --no-verify to skip the lint-staged vitest/no-conditional-expect
flag that disagrees with CI's lint config (same situation as PR #197).

Co-authored-by: Automaker <automaker@localhost>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tanzhenxin added a commit that referenced this pull request May 9, 2026
…2953)

* feat(core): support QWEN_CONFIG_DIR env var to customize config directory

Allow users to override the default ~/.qwen config directory location
via the QWEN_CONFIG_DIR environment variable. This enables users on dev
machines with external disk mounts or custom home directory layouts to
persist config at a location of their choosing.

Changes:
- Add QWEN_CONFIG_DIR check to Storage.getGlobalQwenDir() (absolute and
  relative path support)
- Eliminate 11 redundant '.qwen' constant definitions across packages
- Replace 16+ direct os.homedir() + '.qwen' path constructions with
  Storage.getGlobalQwenDir() calls
- Inline env var checks for packages that cannot import from core
  (channels, vscode-ide-companion, standalone scripts)
- Add unit tests for the new env var behavior
- Project-level .qwen/ directories are NOT affected

Closes #2951

* fix(core): use path.resolve/join in QWEN_CONFIG_DIR tests for Windows compat

Hardcoded Unix paths like '/tmp/custom-qwen/settings.json' fail on
Windows where path APIs produce backslash separators. Use path.resolve()
for inputs and path.join() for assertions so the tests pass cross-platform.

* test(cli): remove flaky 'should keep restart prompt when switching scopes' test

Timing-sensitive UI test that fails intermittently on Windows CI due to
async ANSI output not settling within the wait window.

* feat(core): route remaining hardcoded ~/.qwen/ paths through Storage.getGlobalQwenDir()

Update channel status, memory command, extension storage, skills
discovery, and memory discovery to use Storage.getGlobalQwenDir()
instead of hardcoded os.homedir()/.qwen paths, ensuring QWEN_CONFIG_DIR
env var is respected throughout the codebase.

* fix(tests): mock os.homedir before makeFakeConfig for Storage.getGlobalQwenDir

Storage.getGlobalQwenDir() is now called during Config construction,
which requires os.homedir() to be mocked before makeFakeConfig() is
called. Also mock Storage.getGlobalQwenDir in memoryCommand tests
since it uses a cross-package import that vi.spyOn doesn't intercept.

* fix(core): respect QWEN_CONFIG_DIR for .env discovery and install source

findEnvFile() walk-up would find legacy ~/.qwen/.env before checking
QWEN_CONFIG_DIR/.env when the workspace was under $HOME. Skip the
legacy path when a custom config dir is set so the fallback picks up
the correct file.

Also add a legacy fallback in readSourceInfo() since the installer
always writes source.json to ~/.qwen/ regardless of QWEN_CONFIG_DIR.

* refactor(core): rename QWEN_CONFIG_DIR to QWEN_HOME and fix runtime path resolution

Rename the env var before it ships (zero existing users) to match the
convention of CARGO_HOME, GRADLE_USER_HOME, etc. — "HOME" means "root of
all tool state", not just config.

Key changes:
- Rename QWEN_CONFIG_DIR → QWEN_HOME across all packages and scripts
- Add shared path utils in vscode-ide-companion and channels/base to
  eliminate scattered inline env var resolution
- Fix runtime path mismatch: IDE lock files and session paths in the
  vscode extension now route through getRuntimeBaseDir() (checking
  QWEN_RUNTIME_DIR first), matching core Storage behavior
- Fix telemetry_utils.js otel path to check QWEN_RUNTIME_DIR for tmp/
- Add E2E integration tests for QWEN_HOME scenarios

* fix(core): address critical review issues for QWEN_HOME support

Pass resolved QWEN_HOME as a dedicated QWEN_DIR sandbox parameter so
macOS Seatbelt profiles allow writes to custom config directories.
Fix hookRunner treating signal-killed hooks as success by using ?? -1
instead of || 0. Add QWEN_HOME and QWEN_RUNTIME_DIR to the env vars
documentation table.

* fix(sandbox): whitelist QWEN_RUNTIME_DIR in macOS Seatbelt profiles

When QWEN_RUNTIME_DIR is set separately from QWEN_HOME, the sandbox
was blocking writes to the runtime directory (debug logs, chat history,
IDE locks, sessions). Pass RUNTIME_DIR as a sandbox parameter and add
the corresponding subpath rule to all six .sb profiles.

* fix(core): add tilde expansion to QWEN_HOME and align satellite path helpers

- Extract resolvePath() from resolveRuntimeBaseDir() so QWEN_HOME gets
  the same ~/tilde expansion that QWEN_RUNTIME_DIR already had.
- Port resolvePath() to vscode-ide-companion and channels/base mirrors,
  fixing tilde handling in getRuntimeBaseDir() for the IDE companion.
- Add missing os.tmpdir() fallback in channels/base getGlobalQwenDir().
- Add unit tests for tilde expansion in QWEN_HOME.
- Clarify prompts.ts comment that system.md default is global, not
  project-level.

* fix(core): add tilde expansion to scripts and fix extension cache QWEN_HOME support

Add resolvePath() helper to standalone JS scripts (sandbox_command.js,
telemetry.js, telemetry_utils.js) so QWEN_HOME=~/custom expands
consistently with core Storage.resolvePath().

Fix ExtensionManager.refreshCache() to use ExtensionStorage.getUserExtensionsDir()
instead of hardcoded os.homedir(), so extensions installed under a custom
QWEN_HOME are discoverable.

* test: remove flaky InputPrompt tab-suggestion test on Windows

* test: remove flaky tests that fail intermittently on Windows

Removes 'does not accept the prompt suggestion on shift+tab' from
InputPrompt.test.tsx and 'should keep restart prompt when switching
scopes' from SettingsDialog.test.tsx. Both have been observed to fail
intermittently on the Windows CI workers; the underlying behaviors are
covered by adjacent assertions and end-to-end tests.

* revert(core): keep system.md path project-local under .qwen/

The QWEN_HOME refactor incorrectly routed the QWEN_SYSTEM_MD default path
through Storage.getGlobalQwenDir() (i.e. ~/.qwen/system.md or
$QWEN_HOME/system.md). The original semantics — inherited from the
upstream Gemini-CLI sync — are project-local: <cwd>/.qwen/system.md.

System-prompt customization is intentionally per-project so that each
repository can ship its own override without global side effects. Users
who want a global override can still set QWEN_SYSTEM_MD to an absolute
path. This revert keeps that behavior intact while leaving the rest of
the QWEN_HOME plumbing (settings, credentials, extensions, skills, memory)
unchanged.

* refactor(core): unify QWEN_CONFIG_DIR into the canonical QWEN_DIR

Three definitions of the literal '.qwen' string existed across the
codebase:

- QWEN_DIR in config/storage.ts (canonical, used by the Storage class)
- QWEN_CONFIG_DIR in memory/const.ts
- QWEN_CONFIG_DIR in tools/memory-config.ts (a near-clone of the above)

The QWEN_CONFIG_DIR name also collided with a former env-var name (now
renamed to QWEN_HOME on this branch), making it ambiguous whether call
sites referred to a configurable env var or a hardcoded directory name.

Drop the duplicates and route the only call sites (prompts.ts and its
test) through QWEN_DIR from config/storage.ts. The mock factory in
config.test.ts is updated to no longer expose the removed export.

* fix(integration-tests): use 'extensions list' to trigger settings migration

Tests 2b and 3a in cli/qwen-config-dir.test.ts relied on running
\`qwen --help\` to invoke loadSettings() (and thus the V1→V3 settings
migration). That worked when loadSettings() ran before parseArguments()
in the CLI startup sequence. Main has since flipped the order:
parseArguments() runs first, and yargs intercepts --help and exits the
process before loadSettings() is reached, so migration never runs and
the tests' migration probe always reads back V1.

Switch to \`qwen extensions list\` instead. It is a yargs subcommand that
runs through main() to loadSettings() without requiring an API key, so
migration runs as expected. Update the inline comments to document why
--help cannot be used and why this command works.

* fix(memory): route auto-memory base dir through Storage.getGlobalQwenDir()

The auto-memory subsystem (introduced on main in #3087) computed its base
directory by hardcoding path.join(os.homedir(), QWEN_DIR). That bypassed
QWEN_HOME entirely, so global auto-memory artifacts always landed in
~/.qwen/projects/... regardless of the user's configured QWEN_HOME path.

Route the default through Storage.getGlobalQwenDir() so QWEN_HOME is
honored. The QWEN_CODE_MEMORY_BASE_DIR test override stays as the
highest-priority short-circuit.

Discovered while running the QWEN_HOME e2e test plan against the merged
branch — Group B test B3 (memory tool writes to QWEN_HOME) was the only
failing scenario across A/B/C/D groups.

* fix(cli): treat custom QWEN_HOME .env as user-level

When QWEN_HOME points to a directory whose path does not contain
`.qwen` (e.g., `/tmp/qwen-home`), the global `.env` was misclassified
as a project-level env file. As a result, default-excluded variables
such as `DEBUG` and `DEBUG_MODE` were silently dropped even though
they came from the user-level config directory.

The classification now reuses the same user-level path set computed
by `findEnvFile`, so any `.env` inside the resolved global Qwen
directory (or directly under `~/`) is recognized as user-level.

Also drop the misleading "does not expand `~`" note from the
QWEN_HOME documentation — `Storage.getGlobalQwenDir` does expand
leading tildes via `Storage.resolvePath`.

* fix(cli): drop legacy .qwen substring check from env-file classification

The user-level env-file detection now keys solely off the precomputed
user-level path set, which already covers ~/.env and ${QWEN_HOME}/.env.
The legacy substring fallback misclassified <repo>/.qwen/.env as
user-level, so excludedEnvVars no longer applied to it.

* fix(core): align plain-text hook output with documented exit-code semantics

Per docs/users/features/hooks.md, only exit code 2 is a blocking error;
all other non-zero exit codes are non-blocking and execution should
continue. The plain-text branch in convertPlainTextToHookOutput
previously denied on every non-zero, non-1 exit code (3, 127, signal
fallbacks), contradicting the documented behavior.

Collapse all non-blocking non-zero codes to EXIT_CODE_NON_BLOCKING_ERROR
before passing into the converter so they take the warning path
consistently.

* chore: trigger CI

* fix(cli): pass QWEN_HOME and QWEN_RUNTIME_DIR into docker/podman sandbox

The container CLI previously had no awareness of the host's QWEN_HOME or
QWEN_RUNTIME_DIR values. The global qwen dir worked only because the
mount target happens to match the default fallback inside the sandbox,
and the runtime base dir was lost entirely when it diverged from the
global qwen dir.

* fix(cli): canonicalize sandbox QWEN/RUNTIME paths and pin IDE lock dir

Two reviewer-flagged issues from PR #2953:

* macOS Seatbelt was passed `path.resolve` for `QWEN_DIR`/`RUNTIME_DIR`
  while neighbouring directories used `fs.realpathSync`. With a symlinked
  `QWEN_HOME` or `QWEN_RUNTIME_DIR`, sandbox-exec would compare against
  the canonical kernel path and deny writes. Create the dirs (so
  `realpathSync` can succeed on first run) then canonicalize them like
  the surrounding entries.

* The VS Code companion wrote IDE lock files via the runtime base dir
  while the CLI side resolves the runtime dir from settings too. That
  divergence silently desynced lock-file discovery whenever a user set
  `advanced.runtimeOutputDir` without `QWEN_RUNTIME_DIR`. Anchor both
  sides to `getGlobalQwenDir()` since the companion process can only
  see env vars, not CLI settings.

* fix(cli): finish QWEN_HOME plumbing across env, memory, rules, sandbox

Codex review surfaced four user-visible spots where QWEN_HOME wasn't
threaded through:

* `findEnvFile` walked through the user home dir before consulting the
  QWEN_HOME fallback, so `~/.env` shadowed `<QWEN_HOME>/.env` and
  reversed the qwen-specific precedence the default `~/.qwen/.env` path
  enjoys. Add a home-dir-step check that prefers the custom Qwen dir
  when set.

* `MemoryDialog` displayed and edited `~/.qwen/QWEN.md` regardless of
  QWEN_HOME. Memory discovery already routes through Storage, so user
  edits via the dialog were silently ignored at runtime. Route the
  dialog through `Storage.getGlobalQwenDir()` to match.

* `loadRules` looked up global rules at `~/.qwen/rules/`, ignoring
  QWEN_HOME entirely. Use the global Qwen dir like the rest of the
  config surfaces.

* The Docker/Podman sandbox path called `mkdirSync(userSettingsDir)`
  without `recursive`. Pre-PR the dir was always `~/.qwen` and the
  parent existed; with a nested QWEN_HOME like `/tmp/qwen/config` the
  first run threw ENOENT before the mount could be added.

* fix(cli): block project .env from redirecting QWEN_HOME and QWEN_RUNTIME_DIR

A project `.env` could set QWEN_HOME after settings were already loaded
from the real home, splitting global state: settings.json read from
~/.qwen but later writes (installation_id, OAuth credentials, MCP tokens)
landed in the project-controlled directory. The user-configurable
excludedEnvVars list isn't the right place for this — it's a correctness
boundary, not a preference — so always exclude these two vars from
project .env files. User-level .env files (~/.qwen/.env) are unaffected.

* fix(cli): keep workspace .qwen/.env unfiltered and pre-resolve user QWEN_HOME

The env-file classification conflated two concerns: which paths may
override global state vars, and which paths are exempt from the
user-configurable excludedEnvVars filter. Splitting them lets a
workspace `<repo>/.qwen/.env` carry DEBUG/DEBUG_MODE per the documented
contract while still being blocked from redirecting QWEN_HOME or
QWEN_RUNTIME_DIR.

A QWEN_HOME set in `~/.qwen/.env` or `~/.env` would also previously
arrive too late: USER_SETTINGS_PATH was captured at module load and
loadSettings migrated `~/.qwen/settings.json` before loadEnvironment
applied the override, leaving credentials, MCP tokens, and
installation_id pointed at the new directory while settings stayed at
the legacy one. A pre-pass now reads those user-level files for the
two storage-controlling vars before any user settings are loaded, and
the user settings path is re-resolved locally so all global state lands
in one place.

* fix(cli): make user-settings paths lazy to pick up bootstrapped QWEN_HOME

USER_SETTINGS_PATH/USER_SETTINGS_DIR in settings.ts and the duplicate
USER_SETTINGS_DIR in trustedFolders.ts were top-level consts evaluated
at module load — before preResolveHomeEnvOverrides() reads QWEN_HOME
from ~/.env or ~/.qwen/.env. Callers (sandbox launcher, trusted-folders
reader) saw the legacy ~/.qwen path while the main CLI had moved to the
custom home, splitting state.

Convert all three to lazy getter functions and add a regression test
that pokes process.env.QWEN_HOME after import and asserts each getter
reflects it — any future top-level capture turns the test red.

Mirror the same ~/.env / ~/.qwen/.env bootstrap into
scripts/sandbox_command.js, which previously only read process.env
directly and could disagree with the main CLI on the sandbox setting.

Addresses review threads #3159793469, #3177804507, and item #2 of the
2026-05-06 review summary.

* fix(cli): address qwen home review follow-ups

* test(cli): normalize path in QWEN_HOME freshness assertion for Windows

`getUserSettingsDir()` returns `path.dirname(...)`, which on Windows uses
backslash separators. The bare string comparison failed on Windows runners
("\tmp\qwen-lazy-test" vs "/tmp/qwen-lazy-test"). Wrap the expected value
in `path.normalize()` to match the OS-native separator, mirroring the two
sibling assertions that already use `path.join()`.

* fix(cli): close storage-routing leaks via settings.env and project sandbox .env

settings.env (merged) was being applied to process.env without filtering, so
a workspace settings.json could redirect global state by setting
env.QWEN_HOME or env.QWEN_RUNTIME_DIR after the home-scoped .env bootstrap
ran. Apply PROJECT_ENV_HARDCODED_EXCLUSIONS to the settings.env path too.

scripts/sandbox_command.js's project-walk fallback called dotenv.config() to
find QWEN_SANDBOX, which injected every parsed key — including QWEN_HOME /
QWEN_RUNTIME_DIR the main CLI hard-blocks. Replace with a manual parse that
copies only QWEN_SANDBOX.

Add a startup migration warning when QWEN_HOME points to a directory with
no settings.json while ~/.qwen/settings.json exists, so users notice that
their existing OAuth tokens / settings / memory aren't auto-migrated.

* test: cover QWEN_HOME / QWEN_RUNTIME_DIR in duplicated path helpers

Adds targeted unit tests for the two TypeScript mirrors of
Storage.getGlobalQwenDir() / getRuntimeBaseDir() that live outside
packages/core to avoid cross-package imports. Covers default, absolute,
relative, ~/x, ~\x, and bare ~ inputs, plus the runtime/home priority
chain in the IDE companion.

* fix: bootstrap QWEN_HOME before yargs handlers and in VS Code companion

Two storage-routing leaks surfaced by Codex review of feat/qwen-config-dir:

- channel status/stop call readServiceInfo() inside yargs handlers that
  process.exit before loadSettings() runs, so QWEN_HOME defined only in
  ~/.qwen/.env or ~/.env never resolved for them. The same race exists
  for the duplicate-instance check at the top of channel start. Hoist
  preResolveHomeEnvOverrides() to the top of main() so all subcommand
  handlers see the bootstrapped env vars.

- The VS Code companion's getGlobalQwenDir / getRuntimeBaseDir read
  process.env directly, missing the same .env pre-pass. If a user only
  configures QWEN_HOME via ~/.qwen/.env, the CLI looks under the
  redirected dir while the companion writes IDE lock files under
  ~/.qwen, breaking IDE discovery. Mirror the CLI pre-pass in the
  companion (lazy, idempotent) without importing from core.

* fix(config): preserve credentials in legacy ~/.qwen/.env when QWEN_HOME redirects

When QWEN_HOME is bootstrapped from `~/.qwen/.env`, the home-dir env walk
previously skipped that file and never read `<QWEN_HOME>/.env` from the
companion. This stranded non-routing credentials (e.g. OPENAI_API_KEY)
left in `~/.qwen/.env` and let the companion write IDE lock files into a
different runtime dir than the CLI was reading from.

- CLI: fall back to `~/.qwen/.env` after `<QWEN_HOME>/.env` at both the
  home-dir step and the post-walk fallback in findEnvFile, and treat the
  legacy path as user-level for trust and exclusion semantics.
- Companion: after the initial candidate pass discovers QWEN_HOME, also
  read `<QWEN_HOME>/.env` so QWEN_RUNTIME_DIR sourced from there matches
  what the CLI's findEnvFile would pick.

* refactor(cli): simplify QWEN_HOME plumbing — dedupe helpers, latch, comments

- replace local isSameOrChildPath with core's isSubpath in sandbox.ts
- latch preResolveHomeEnvOverrides so it runs once per process
- pass userLevelPaths from loadEnvironment into findEnvFile (no recompute)
- collapse findEnvFile's home-dir branch and post-loop fallback into one
  shared candidate list (drops duplicate existsSync calls)
- factor extensionManager's user-extensions loop into a private helper
- use QWEN_DIR constant instead of '.qwen' literal in skill-manager
- trim narrative / PR-history comments across changed files

* fix(cli): align QWEN_HOME .env bootstrap across CLI, sandbox, telemetry

Telemetry scripts previously read process.env.QWEN_HOME directly, so a
QWEN_HOME set only in ~/.env or ~/.qwen/.env left telemetry writing to
~/.qwen while the CLI routed elsewhere. Extract the bootstrap into
scripts/lib/qwen-home-bootstrap.js and have sandbox_command.js,
telemetry.js, and telemetry_utils.js share it.

Also add a third-pass <new QWEN_HOME>/.env read in
preResolveHomeEnvOverrides so the CLI and VS Code companion agree on
QWEN_RUNTIME_DIR when it is configured under the new home dir.

* test(integration-tests): update QWEN_HOME assertions for v4 schema

Settings schema was bumped to v4 on main (gitCoAuthor migration). The
qwen-config-dir tests still asserted post-migration $version === 3, so
they failed after the merge. Bump the assertions to 4 and the seed in
3a to match, and point a comment at SETTINGS_VERSION so the next bump
is easy to find.
TaimoorSiddiquiOfficial pushed a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request May 15, 2026
…wenLM#2953)

* feat(core): support QWEN_CONFIG_DIR env var to customize config directory

Allow users to override the default ~/.qwen config directory location
via the QWEN_CONFIG_DIR environment variable. This enables users on dev
machines with external disk mounts or custom home directory layouts to
persist config at a location of their choosing.

Changes:
- Add QWEN_CONFIG_DIR check to Storage.getGlobalQwenDir() (absolute and
  relative path support)
- Eliminate 11 redundant '.qwen' constant definitions across packages
- Replace 16+ direct os.homedir() + '.qwen' path constructions with
  Storage.getGlobalQwenDir() calls
- Inline env var checks for packages that cannot import from core
  (channels, vscode-ide-companion, standalone scripts)
- Add unit tests for the new env var behavior
- Project-level .qwen/ directories are NOT affected

Closes QwenLM#2951

* fix(core): use path.resolve/join in QWEN_CONFIG_DIR tests for Windows compat

Hardcoded Unix paths like '/tmp/custom-qwen/settings.json' fail on
Windows where path APIs produce backslash separators. Use path.resolve()
for inputs and path.join() for assertions so the tests pass cross-platform.

* test(cli): remove flaky 'should keep restart prompt when switching scopes' test

Timing-sensitive UI test that fails intermittently on Windows CI due to
async ANSI output not settling within the wait window.

* feat(core): route remaining hardcoded ~/.qwen/ paths through Storage.getGlobalQwenDir()

Update channel status, memory command, extension storage, skills
discovery, and memory discovery to use Storage.getGlobalQwenDir()
instead of hardcoded os.homedir()/.qwen paths, ensuring QWEN_CONFIG_DIR
env var is respected throughout the codebase.

* fix(tests): mock os.homedir before makeFakeConfig for Storage.getGlobalQwenDir

Storage.getGlobalQwenDir() is now called during Config construction,
which requires os.homedir() to be mocked before makeFakeConfig() is
called. Also mock Storage.getGlobalQwenDir in memoryCommand tests
since it uses a cross-package import that vi.spyOn doesn't intercept.

* fix(core): respect QWEN_CONFIG_DIR for .env discovery and install source

findEnvFile() walk-up would find legacy ~/.qwen/.env before checking
QWEN_CONFIG_DIR/.env when the workspace was under $HOME. Skip the
legacy path when a custom config dir is set so the fallback picks up
the correct file.

Also add a legacy fallback in readSourceInfo() since the installer
always writes source.json to ~/.qwen/ regardless of QWEN_CONFIG_DIR.

* refactor(core): rename QWEN_CONFIG_DIR to QWEN_HOME and fix runtime path resolution

Rename the env var before it ships (zero existing users) to match the
convention of CARGO_HOME, GRADLE_USER_HOME, etc. — "HOME" means "root of
all tool state", not just config.

Key changes:
- Rename QWEN_CONFIG_DIR → QWEN_HOME across all packages and scripts
- Add shared path utils in vscode-ide-companion and channels/base to
  eliminate scattered inline env var resolution
- Fix runtime path mismatch: IDE lock files and session paths in the
  vscode extension now route through getRuntimeBaseDir() (checking
  QWEN_RUNTIME_DIR first), matching core Storage behavior
- Fix telemetry_utils.js otel path to check QWEN_RUNTIME_DIR for tmp/
- Add E2E integration tests for QWEN_HOME scenarios

* fix(core): address critical review issues for QWEN_HOME support

Pass resolved QWEN_HOME as a dedicated QWEN_DIR sandbox parameter so
macOS Seatbelt profiles allow writes to custom config directories.
Fix hookRunner treating signal-killed hooks as success by using ?? -1
instead of || 0. Add QWEN_HOME and QWEN_RUNTIME_DIR to the env vars
documentation table.

* fix(sandbox): whitelist QWEN_RUNTIME_DIR in macOS Seatbelt profiles

When QWEN_RUNTIME_DIR is set separately from QWEN_HOME, the sandbox
was blocking writes to the runtime directory (debug logs, chat history,
IDE locks, sessions). Pass RUNTIME_DIR as a sandbox parameter and add
the corresponding subpath rule to all six .sb profiles.

* fix(core): add tilde expansion to QWEN_HOME and align satellite path helpers

- Extract resolvePath() from resolveRuntimeBaseDir() so QWEN_HOME gets
  the same ~/tilde expansion that QWEN_RUNTIME_DIR already had.
- Port resolvePath() to vscode-ide-companion and channels/base mirrors,
  fixing tilde handling in getRuntimeBaseDir() for the IDE companion.
- Add missing os.tmpdir() fallback in channels/base getGlobalQwenDir().
- Add unit tests for tilde expansion in QWEN_HOME.
- Clarify prompts.ts comment that system.md default is global, not
  project-level.

* fix(core): add tilde expansion to scripts and fix extension cache QWEN_HOME support

Add resolvePath() helper to standalone JS scripts (sandbox_command.js,
telemetry.js, telemetry_utils.js) so QWEN_HOME=~/custom expands
consistently with core Storage.resolvePath().

Fix ExtensionManager.refreshCache() to use ExtensionStorage.getUserExtensionsDir()
instead of hardcoded os.homedir(), so extensions installed under a custom
QWEN_HOME are discoverable.

* test: remove flaky InputPrompt tab-suggestion test on Windows

* test: remove flaky tests that fail intermittently on Windows

Removes 'does not accept the prompt suggestion on shift+tab' from
InputPrompt.test.tsx and 'should keep restart prompt when switching
scopes' from SettingsDialog.test.tsx. Both have been observed to fail
intermittently on the Windows CI workers; the underlying behaviors are
covered by adjacent assertions and end-to-end tests.

* revert(core): keep system.md path project-local under .qwen/

The QWEN_HOME refactor incorrectly routed the QWEN_SYSTEM_MD default path
through Storage.getGlobalQwenDir() (i.e. ~/.qwen/system.md or
$QWEN_HOME/system.md). The original semantics — inherited from the
upstream Gemini-CLI sync — are project-local: <cwd>/.qwen/system.md.

System-prompt customization is intentionally per-project so that each
repository can ship its own override without global side effects. Users
who want a global override can still set QWEN_SYSTEM_MD to an absolute
path. This revert keeps that behavior intact while leaving the rest of
the QWEN_HOME plumbing (settings, credentials, extensions, skills, memory)
unchanged.

* refactor(core): unify QWEN_CONFIG_DIR into the canonical QWEN_DIR

Three definitions of the literal '.qwen' string existed across the
codebase:

- QWEN_DIR in config/storage.ts (canonical, used by the Storage class)
- QWEN_CONFIG_DIR in memory/const.ts
- QWEN_CONFIG_DIR in tools/memory-config.ts (a near-clone of the above)

The QWEN_CONFIG_DIR name also collided with a former env-var name (now
renamed to QWEN_HOME on this branch), making it ambiguous whether call
sites referred to a configurable env var or a hardcoded directory name.

Drop the duplicates and route the only call sites (prompts.ts and its
test) through QWEN_DIR from config/storage.ts. The mock factory in
config.test.ts is updated to no longer expose the removed export.

* fix(integration-tests): use 'extensions list' to trigger settings migration

Tests 2b and 3a in cli/qwen-config-dir.test.ts relied on running
\`qwen --help\` to invoke loadSettings() (and thus the V1→V3 settings
migration). That worked when loadSettings() ran before parseArguments()
in the CLI startup sequence. Main has since flipped the order:
parseArguments() runs first, and yargs intercepts --help and exits the
process before loadSettings() is reached, so migration never runs and
the tests' migration probe always reads back V1.

Switch to \`qwen extensions list\` instead. It is a yargs subcommand that
runs through main() to loadSettings() without requiring an API key, so
migration runs as expected. Update the inline comments to document why
--help cannot be used and why this command works.

* fix(memory): route auto-memory base dir through Storage.getGlobalQwenDir()

The auto-memory subsystem (introduced on main in QwenLM#3087) computed its base
directory by hardcoding path.join(os.homedir(), QWEN_DIR). That bypassed
QWEN_HOME entirely, so global auto-memory artifacts always landed in
~/.qwen/projects/... regardless of the user's configured QWEN_HOME path.

Route the default through Storage.getGlobalQwenDir() so QWEN_HOME is
honored. The QWEN_CODE_MEMORY_BASE_DIR test override stays as the
highest-priority short-circuit.

Discovered while running the QWEN_HOME e2e test plan against the merged
branch — Group B test B3 (memory tool writes to QWEN_HOME) was the only
failing scenario across A/B/C/D groups.

* fix(cli): treat custom QWEN_HOME .env as user-level

When QWEN_HOME points to a directory whose path does not contain
`.qwen` (e.g., `/tmp/qwen-home`), the global `.env` was misclassified
as a project-level env file. As a result, default-excluded variables
such as `DEBUG` and `DEBUG_MODE` were silently dropped even though
they came from the user-level config directory.

The classification now reuses the same user-level path set computed
by `findEnvFile`, so any `.env` inside the resolved global Qwen
directory (or directly under `~/`) is recognized as user-level.

Also drop the misleading "does not expand `~`" note from the
QWEN_HOME documentation — `Storage.getGlobalQwenDir` does expand
leading tildes via `Storage.resolvePath`.

* fix(cli): drop legacy .qwen substring check from env-file classification

The user-level env-file detection now keys solely off the precomputed
user-level path set, which already covers ~/.env and ${QWEN_HOME}/.env.
The legacy substring fallback misclassified <repo>/.qwen/.env as
user-level, so excludedEnvVars no longer applied to it.

* fix(core): align plain-text hook output with documented exit-code semantics

Per docs/users/features/hooks.md, only exit code 2 is a blocking error;
all other non-zero exit codes are non-blocking and execution should
continue. The plain-text branch in convertPlainTextToHookOutput
previously denied on every non-zero, non-1 exit code (3, 127, signal
fallbacks), contradicting the documented behavior.

Collapse all non-blocking non-zero codes to EXIT_CODE_NON_BLOCKING_ERROR
before passing into the converter so they take the warning path
consistently.

* chore: trigger CI

* fix(cli): pass QWEN_HOME and QWEN_RUNTIME_DIR into docker/podman sandbox

The container CLI previously had no awareness of the host's QWEN_HOME or
QWEN_RUNTIME_DIR values. The global qwen dir worked only because the
mount target happens to match the default fallback inside the sandbox,
and the runtime base dir was lost entirely when it diverged from the
global qwen dir.

* fix(cli): canonicalize sandbox QWEN/RUNTIME paths and pin IDE lock dir

Two reviewer-flagged issues from PR QwenLM#2953:

* macOS Seatbelt was passed `path.resolve` for `QWEN_DIR`/`RUNTIME_DIR`
  while neighbouring directories used `fs.realpathSync`. With a symlinked
  `QWEN_HOME` or `QWEN_RUNTIME_DIR`, sandbox-exec would compare against
  the canonical kernel path and deny writes. Create the dirs (so
  `realpathSync` can succeed on first run) then canonicalize them like
  the surrounding entries.

* The VS Code companion wrote IDE lock files via the runtime base dir
  while the CLI side resolves the runtime dir from settings too. That
  divergence silently desynced lock-file discovery whenever a user set
  `advanced.runtimeOutputDir` without `QWEN_RUNTIME_DIR`. Anchor both
  sides to `getGlobalQwenDir()` since the companion process can only
  see env vars, not CLI settings.

* fix(cli): finish QWEN_HOME plumbing across env, memory, rules, sandbox

Codex review surfaced four user-visible spots where QWEN_HOME wasn't
threaded through:

* `findEnvFile` walked through the user home dir before consulting the
  QWEN_HOME fallback, so `~/.env` shadowed `<QWEN_HOME>/.env` and
  reversed the qwen-specific precedence the default `~/.qwen/.env` path
  enjoys. Add a home-dir-step check that prefers the custom Qwen dir
  when set.

* `MemoryDialog` displayed and edited `~/.qwen/QWEN.md` regardless of
  QWEN_HOME. Memory discovery already routes through Storage, so user
  edits via the dialog were silently ignored at runtime. Route the
  dialog through `Storage.getGlobalQwenDir()` to match.

* `loadRules` looked up global rules at `~/.qwen/rules/`, ignoring
  QWEN_HOME entirely. Use the global Qwen dir like the rest of the
  config surfaces.

* The Docker/Podman sandbox path called `mkdirSync(userSettingsDir)`
  without `recursive`. Pre-PR the dir was always `~/.qwen` and the
  parent existed; with a nested QWEN_HOME like `/tmp/qwen/config` the
  first run threw ENOENT before the mount could be added.

* fix(cli): block project .env from redirecting QWEN_HOME and QWEN_RUNTIME_DIR

A project `.env` could set QWEN_HOME after settings were already loaded
from the real home, splitting global state: settings.json read from
~/.qwen but later writes (installation_id, OAuth credentials, MCP tokens)
landed in the project-controlled directory. The user-configurable
excludedEnvVars list isn't the right place for this — it's a correctness
boundary, not a preference — so always exclude these two vars from
project .env files. User-level .env files (~/.qwen/.env) are unaffected.

* fix(cli): keep workspace .qwen/.env unfiltered and pre-resolve user QWEN_HOME

The env-file classification conflated two concerns: which paths may
override global state vars, and which paths are exempt from the
user-configurable excludedEnvVars filter. Splitting them lets a
workspace `<repo>/.qwen/.env` carry DEBUG/DEBUG_MODE per the documented
contract while still being blocked from redirecting QWEN_HOME or
QWEN_RUNTIME_DIR.

A QWEN_HOME set in `~/.qwen/.env` or `~/.env` would also previously
arrive too late: USER_SETTINGS_PATH was captured at module load and
loadSettings migrated `~/.qwen/settings.json` before loadEnvironment
applied the override, leaving credentials, MCP tokens, and
installation_id pointed at the new directory while settings stayed at
the legacy one. A pre-pass now reads those user-level files for the
two storage-controlling vars before any user settings are loaded, and
the user settings path is re-resolved locally so all global state lands
in one place.

* fix(cli): make user-settings paths lazy to pick up bootstrapped QWEN_HOME

USER_SETTINGS_PATH/USER_SETTINGS_DIR in settings.ts and the duplicate
USER_SETTINGS_DIR in trustedFolders.ts were top-level consts evaluated
at module load — before preResolveHomeEnvOverrides() reads QWEN_HOME
from ~/.env or ~/.qwen/.env. Callers (sandbox launcher, trusted-folders
reader) saw the legacy ~/.qwen path while the main CLI had moved to the
custom home, splitting state.

Convert all three to lazy getter functions and add a regression test
that pokes process.env.QWEN_HOME after import and asserts each getter
reflects it — any future top-level capture turns the test red.

Mirror the same ~/.env / ~/.qwen/.env bootstrap into
scripts/sandbox_command.js, which previously only read process.env
directly and could disagree with the main CLI on the sandbox setting.

Addresses review threads #3159793469, #3177804507, and item #2 of the
2026-05-06 review summary.

* fix(cli): address qwen home review follow-ups

* test(cli): normalize path in QWEN_HOME freshness assertion for Windows

`getUserSettingsDir()` returns `path.dirname(...)`, which on Windows uses
backslash separators. The bare string comparison failed on Windows runners
("\tmp\qwen-lazy-test" vs "/tmp/qwen-lazy-test"). Wrap the expected value
in `path.normalize()` to match the OS-native separator, mirroring the two
sibling assertions that already use `path.join()`.

* fix(cli): close storage-routing leaks via settings.env and project sandbox .env

settings.env (merged) was being applied to process.env without filtering, so
a workspace settings.json could redirect global state by setting
env.QWEN_HOME or env.QWEN_RUNTIME_DIR after the home-scoped .env bootstrap
ran. Apply PROJECT_ENV_HARDCODED_EXCLUSIONS to the settings.env path too.

scripts/sandbox_command.js's project-walk fallback called dotenv.config() to
find QWEN_SANDBOX, which injected every parsed key — including QWEN_HOME /
QWEN_RUNTIME_DIR the main CLI hard-blocks. Replace with a manual parse that
copies only QWEN_SANDBOX.

Add a startup migration warning when QWEN_HOME points to a directory with
no settings.json while ~/.qwen/settings.json exists, so users notice that
their existing OAuth tokens / settings / memory aren't auto-migrated.

* test: cover QWEN_HOME / QWEN_RUNTIME_DIR in duplicated path helpers

Adds targeted unit tests for the two TypeScript mirrors of
Storage.getGlobalQwenDir() / getRuntimeBaseDir() that live outside
packages/core to avoid cross-package imports. Covers default, absolute,
relative, ~/x, ~\x, and bare ~ inputs, plus the runtime/home priority
chain in the IDE companion.

* fix: bootstrap QWEN_HOME before yargs handlers and in VS Code companion

Two storage-routing leaks surfaced by Codex review of feat/qwen-config-dir:

- channel status/stop call readServiceInfo() inside yargs handlers that
  process.exit before loadSettings() runs, so QWEN_HOME defined only in
  ~/.qwen/.env or ~/.env never resolved for them. The same race exists
  for the duplicate-instance check at the top of channel start. Hoist
  preResolveHomeEnvOverrides() to the top of main() so all subcommand
  handlers see the bootstrapped env vars.

- The VS Code companion's getGlobalQwenDir / getRuntimeBaseDir read
  process.env directly, missing the same .env pre-pass. If a user only
  configures QWEN_HOME via ~/.qwen/.env, the CLI looks under the
  redirected dir while the companion writes IDE lock files under
  ~/.qwen, breaking IDE discovery. Mirror the CLI pre-pass in the
  companion (lazy, idempotent) without importing from core.

* fix(config): preserve credentials in legacy ~/.qwen/.env when QWEN_HOME redirects

When QWEN_HOME is bootstrapped from `~/.qwen/.env`, the home-dir env walk
previously skipped that file and never read `<QWEN_HOME>/.env` from the
companion. This stranded non-routing credentials (e.g. OPENAI_API_KEY)
left in `~/.qwen/.env` and let the companion write IDE lock files into a
different runtime dir than the CLI was reading from.

- CLI: fall back to `~/.qwen/.env` after `<QWEN_HOME>/.env` at both the
  home-dir step and the post-walk fallback in findEnvFile, and treat the
  legacy path as user-level for trust and exclusion semantics.
- Companion: after the initial candidate pass discovers QWEN_HOME, also
  read `<QWEN_HOME>/.env` so QWEN_RUNTIME_DIR sourced from there matches
  what the CLI's findEnvFile would pick.

* refactor(cli): simplify QWEN_HOME plumbing — dedupe helpers, latch, comments

- replace local isSameOrChildPath with core's isSubpath in sandbox.ts
- latch preResolveHomeEnvOverrides so it runs once per process
- pass userLevelPaths from loadEnvironment into findEnvFile (no recompute)
- collapse findEnvFile's home-dir branch and post-loop fallback into one
  shared candidate list (drops duplicate existsSync calls)
- factor extensionManager's user-extensions loop into a private helper
- use QWEN_DIR constant instead of '.qwen' literal in skill-manager
- trim narrative / PR-history comments across changed files

* fix(cli): align QWEN_HOME .env bootstrap across CLI, sandbox, telemetry

Telemetry scripts previously read process.env.QWEN_HOME directly, so a
QWEN_HOME set only in ~/.env or ~/.qwen/.env left telemetry writing to
~/.qwen while the CLI routed elsewhere. Extract the bootstrap into
scripts/lib/qwen-home-bootstrap.js and have sandbox_command.js,
telemetry.js, and telemetry_utils.js share it.

Also add a third-pass <new QWEN_HOME>/.env read in
preResolveHomeEnvOverrides so the CLI and VS Code companion agree on
QWEN_RUNTIME_DIR when it is configured under the new home dir.

* test(integration-tests): update QWEN_HOME assertions for v4 schema

Settings schema was bumped to v4 on main (gitCoAuthor migration). The
qwen-config-dir tests still asserted post-migration $version === 3, so
they failed after the merge. Bump the assertions to 4 and the seed in
3a to match, and point a comment at SETTINGS_VERSION so the next bump
is easy to find.

(cherry picked from commit 78ad595)
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
* docs: add auto-memory implementation log

* feat(core): add managed auto-memory storage scaffold

* feat(core): load managed auto-memory index

* feat(core): add managed auto-memory recall

* feat(core): add managed auto-memory extraction

* feat(cli): add managed auto-memory dream commands

* feat(core): add auxiliary side-query foundation

* feat(memory): add model-driven recall selection

* feat(memory): add model-driven extraction planner

* feat(core): add background task runtime foundation

* feat(memory): schedule auto dream in background

* feat(core): add background agent runner foundation

* feat(memory): add extraction agent planner

* feat(core): add dream agent planner

* feat(core): rebuild managed memory index

* feat(memory): add governance status commands

* feat(memory): add managed forget flow

* feat(core): harden background agent planning

* feat(memory): complete managed parity closure

* test(memory): add managed lifecycle integration coverage

* feat: same to cc

* feat(memory-ui): add memory saved notification and memory count badge

Feature 3 - Memory Saved Notification:
- Add HistoryItemMemorySaved type to types.ts
- Create MemorySavedMessage component for rendering '● Saved/Updated N memories'
- In useGeminiStream: detect in-turn memory writes via mapToDisplay's
  memoryWriteCount field and emit 'memory_saved' history item after turn
- In client.ts: capture background dream/extract promises and expose
  via consumePendingMemoryTaskPromises(); useGeminiStream listens
  post-turn and emits 'Updated N memories' notification for background tasks

Feature 4 - Memory Count Badge:
- Add isMemoryOp field to IndividualToolCallDisplay
- Add memoryWriteCount/memoryReadCount to HistoryItemToolGroup
- Add detectMemoryOp() in useReactToolScheduler using isAutoMemPath
- ToolGroupMessage renders '● Recalled N memories, Wrote N memories' badge
  at the top of tool groups that touch memory files

Fix: process.env bracket-access in paths.ts (noPropertyAccessFromIndexSignature)
Fix: MemoryDialog.test.tsx mock useSettings to satisfy SettingsProvider requirement

* fix(memory-ui): auto-approve memory writes, collapse memory tool groups, fix MEMORY.md path

Problem 1 - Auto-approve memory file operations:
- write-file.ts: getDefaultPermission() checks isAutoMemPath; returns 'allow'
  for managed auto-memory files, 'ask' for all other files
- edit.ts: same pattern

Problem 2 - Feature 4 UX: collapse memory-only tool groups:
- ToolGroupMessage: detect when all tool calls have isMemoryOp set (pure memory
  group) and all are complete; render compact '● Recalled/Wrote N memories
  (ctrl+o to expand)' instead of individual tool call rows
- ctrl+o toggles expand/collapse when isFocused and group is memory-only
- Mixed groups (memory + other tools) keep badge-at-top behaviour
- Expanded state shows individual tool calls with '● Memory operations
  (ctrl+o to collapse)' header

Problem 3 - MEMORY.md path mismatch:
- prompt.ts: Step 2 now references full absolute path ${memoryDir}/MEMORY.md
  so the model writes to the correct location inside the memory directory,
  not to the parent project directory

Fix tests:
- write-file.test.ts: add getProjectRoot to mockConfigInternal
- prompt.test.ts: update assertion to match full-path section header

* fix(memory-ui): fix duplicate notification, broken ctrl+o, and Edit tool detection

- Remove duplicate 'Saved N memories' notification: the tool group badge already
  shows 'Wrote N memories'; the separate HistoryItemMemorySaved addItem after
  onComplete was double-counting. Keep only the background-task path
  (consumePendingMemoryTaskPromises).

- Remove ctrl+o expand: Ink's Static area freezes items on first render and
  cannot respond to user input. useInput/useState(isExpanded) in a Static item
  is a no-op. Removed the dead code; memory-only groups now always render as
  the compact summary (no fake interactive hint).

- Fix Edit tool detection: detectMemoryOp was checking for 'edit_file' but the
  real tool name constant is 'edit'. Also removed non-existent 'create_file'
  (write_file covers all writes). Now editing MEMORY.md is correctly identified
  as a memory write op, collapses to 'Wrote N memories', and is auto-approved.

* fix(dream): run /dream as a visible submit_prompt turn, not a silent background agent

The previous implementation ran an AgentHeadless background agent that could
take 5+ minutes with zero UI feedback — user saw a blank screen for the entire
duration and then at most one line of text.

Fix: /dream now returns submit_prompt with the consolidation task prompt so it
runs as a regular AI conversation turn. Tool calls (read_file, write_file, edit,
grep_search, list_directory, glob) are immediately visible as collapsed tool
groups as the model works through the memory files — identical UX to Claude Code.

Also export buildConsolidationTaskPrompt from dreamAgentPlanner so dreamCommand
can reuse the same detailed consolidation prompt that was already written.

* fix(memory): auto-allow ls/glob/grep on memory base directory

Add getMemoryBaseDir() to getDefaultPermission() allow list in ls.ts,
glob.ts, and grep.ts — mirrors the existing pattern in read-file.ts.

Without this, ListFiles/Glob/Grep on ~/.qwen/* would trigger an
approval dialog, blocking /dream at its very first step.

* fix(background): prevent permission prompt hangs in background agents

Match Claude Code's headless-agent intent: background memory agents must never
block on interactive permission prompts.

Wrap background runtime config so getApprovalMode() returns YOLO, ensuring any
ask decision is auto-approved instead of hanging forever. Add regression test
covering the wrapped approval mode.

* fix(memory): run auto extract through forked agent

Make managed auto-memory extraction follow the Claude Code architecture:
background extraction now uses a forked agent to read/write memory files
directly, instead of planning patches and applying them with a separate
filesystem pipeline.

Keep the old patch/model path only as fallback if the forked agent fails.
Add regression tests covering the new execution path and tool whitelist.

* refactor(memory): remove legacy extract fallback pipeline

Delete the old patch/model/heuristic extraction path entirely.
Managed auto-memory extract now runs only through the forked-agent
execution flow, with no planner/apply fallback stages remaining.

Also remove obsolete exports/tests and update scheduler/integration
coverage to use the forked-agent-only architecture.

* refactor(memory): move auxiliary files out of memory/ directory

meta.json, extract-cursor.json, and consolidation.lock are internal
bookkeeping files, not user-visible memories. Move them one level up
to the project state dir (parent of memory/) so that the memory/
directory contains only MEMORY.md and topic files, matching the
clean layout of the upstream reference implementation.

Add getAutoMemoryProjectStateDir() helper in paths.ts and update the
three path accessors + store.test.ts path assertions accordingly.

* fix(memory): record lastDreamAt after manual /dream run

The /dream command submits a prompt to the main agent (submit_prompt),
which writes memory files directly. Because it bypasses dreamScheduler,
meta.json was never updated and /memory always showed 'never'.

Fix by:
- Exporting writeDreamManualRunToMetadata() from dream.ts
- Adding optional onComplete callback to SubmitPromptActionReturn and
  SubmitPromptResult (types.ts / commands/types.ts)
- Propagating onComplete through slashCommandProcessor.ts
- Firing onComplete after turn completion in useGeminiStream.ts
- Providing the callback in dreamCommand.ts to write lastDreamAt

* fix(memory): remove scope params from /remember in managed auto-memory mode

--global/--project are legacy save_memory tool concepts. In managed
auto-memory mode the forked agent decides the appropriate type
(user/feedback/project/reference) based on the content of the fact.

Also improve the prompt wording to explicitly ask the agent to choose
the correct type, reducing the tendency to default to 'project'.

* feat(ui): show '✦ dreaming' indicator in footer during background dream

Subscribe to getManagedAutoMemoryDreamTaskRegistry() in Footer via a
useDreamRunning() hook. While any dream task for the current project is
pending or running, display '✦ dreaming' in the right section of the
footer bar, between Debug Mode and context usage.

* refactor(memory): align dream/extract infrastructure with Claude Code patterns

Five improvements based on Claude Code parity audit:

1. Memoize getAutoMemoryRoot (paths.ts)
   - Add _autoMemoryRootCache Map, keyed by projectRoot
   - findCanonicalGitRoot() walks the filesystem per call; memoize avoids
     repeated git-tree traversal on hot-path schedulers/scanners
   - Expose clearAutoMemoryRootCache() for test teardown

2. Lock file stores PID + isProcessRunning reclaim (dreamScheduler.ts)
   - acquireDreamLock() writes process.pid to the lock file body
   - lockExists() reads PID and calls process.kill(pid, 0); dead/missing
     PID reclaims the lock immediately instead of waiting 2h
   - Stale threshold reduced to 1h (PID-reuse guard, same as CC)

3. Session scan throttle (dreamScheduler.ts)
   - Add SESSION_SCAN_INTERVAL_MS = 10min (same as CC)
   - Add lastSessionScanAt Map<projectRoot, number> to ManagedAutoMemoryDreamRuntime
   - When time-gate passes but session-gate doesn't, throttle prevents
     re-scanning the filesystem on every user turn

4. mtime-based session counting (dreamScheduler.ts)
   - Replace fragile recentSessionIdsSinceDream Set in meta.json with
     filesystem mtime scan (listSessionsTouchedSince)
   - Mirrors Claude Code's listSessionsTouchedSince: reads session JSONL
     files from Storage.getProjectDir()/chats/, filters by mtime > lastDreamAt
   - Immune to meta.json corruption/loss; no per-turn metadata write
   - ManagedAutoMemoryDreamRuntime accepts injectable SessionScannerFn
     for clean unit testing without real session files

5. Extraction mutual exclusion extended to write_file/edit (extractScheduler.ts)
   - historySliceUsesMemoryTool() now checks write_file/edit/replace/create_file
     tool calls whose file_path is within isAutoMemPath()
   - Previously only detected save_memory; missed direct file writes by
     the main agent, causing redundant background extraction

* docs(memory): add user-facing memory docs, i18n for all locales, simplify /forget

- Add docs/users/features/memory.md: comprehensive user-facing guide covering
  QWEN.md instructions, auto-memory behaviour, all memory commands, and
  troubleshooting; replaces the placeholder auto-memory.md
- Update docs/users/features/_meta.ts: rename entry auto-memory → memory
- Update docs/users/features/commands.md: add /init, /remember, /forget,
  /dream rows; fix /memory description; remove /init duplicate
- Update docs/users/configuration/settings.md: add memory.* settings section
  (enableManagedAutoMemory, enableManagedAutoDream) between tools and permissions
- Remove /forget --apply flag: preview-then-apply flow replaced with direct
  deletion; update forgetCommand.ts, en.js, zh.js accordingly
- Add all auto-memory i18n keys to de, ja, pt, ru locales (18 keys each):
  Open auto-memory folder, Auto-memory/Auto-dream status lines, never/on/off,
  ✦ dreaming, /forget and /remember usage strings, all managed-memory messages
- Remove dead save_memory branch from extractScheduler.partWritesToMemory()
- Add ✦ dreaming indicator to Footer.tsx with i18n; fix Footer.test.tsx mocks
- Refactor MemoryDialog.tsx auto-dream status line to use i18n
- Remove save_memory tool (memoryTool.ts/test); clean up webui references
- Add extractionPlanner.ts, const.ts and associated tests
- Delete stale docs/users/configuration/memory.md and
  docs/developers/tools/memory.md (content superseded)

* refactor(memory): remove all Claude Code references from comments and test names

* test(memory): remove empty placeholder test files that cause vitest to fail

* fix eslint

* fix test in windows

* fix test

* fix(memory): address critical review findings from PR QwenLM#3087

- fix(read-file): narrow auto-allow from getMemoryBaseDir() (~/.qwen) to
  isAutoMemPath(projectRoot) to prevent exposing settings.json / OAuth
  credentials without user approval (wenshao review)

- fix(forget): per-entry deletion instead of whole-file unlink
  - assign stable per-entry IDs (relativePath:index for multi-entry files)
    so the model can target individual entries without removing siblings
  - rewrite file keeping unmatched entries; only unlink when file becomes
    empty (wenshao review)

- fix(entries): round-trip correctness for multi-entry new-format bodies
  - parseAutoMemoryEntries: plain-text line closes current entry and opens
    a new one (was silently ignored when current was already set)
  - renderAutoMemoryBody: emit blank line between adjacent entries so the
    parser can detect entry boundaries on re-read (wenshao review)

- fix(entries): resolve two CodeQL polynomial-regex alerts
  - indentedMatch: \s{2,}(?:[-*]\s+)? → [\t ]{2,}(?:[-*][\t ]+)?
  - topLevelMatch: :\s*(.+)$ → :[ \t]*(\S.*)$
  (github-advanced-security review)

- fix(scan.test): use forward-slash literal for relativePath expectation
  since listMarkdownFiles() normalises all separators to '/' on all
  platforms including Windows

* fix(memory): replace isAutoMemPath startsWith with path.relative()

Using path.relative() instead of string startsWith() is more robust
across platforms — it correctly handles Windows path-separator
differences and avoids potential edge cases where a path prefix match
could succeed on non-separator boundaries.

Addresses github-actions review item 3 (PR QwenLM#3087).

* feat(telemetry): add auto-memory telemetry instrumentation

Add OpenTelemetry logs + metrics for the five auto-memory lifecycle
events: extract, dream, recall, forget, and remember.

Telemetry layer (packages/core/src/telemetry/):
- constants.ts: 5 new event-name constants
  (qwen-code.memory.{extract,dream,recall,forget,remember})
- types.ts: 5 new event classes with typed constructor params
  (MemoryExtractEvent, MemoryDreamEvent, MemoryRecallEvent,
   MemoryForgetEvent, MemoryRememberEvent)
- metrics.ts: 8 new OTel instruments (5 Counters + 3 Histograms)
  with recordMemoryXxx() helpers; registered inside initializeMetrics()
- loggers.ts: logMemoryExtract/Dream/Recall/Forget/Remember() — each
  emits a structured log record and calls its recordXxx() counterpart
- index.ts: re-exports all new symbols

Instrumentation call-sites:
- extractScheduler.ts ManagedAutoMemoryExtractRuntime.runTask():
  emits extract event with trigger=auto, completed/failed status,
  patches_count, touched_topics, and wall-clock duration
- dream.ts runManagedAutoMemoryDream():
  emits dream event with trigger=auto, updated/noop status,
  deduped_entries, touched_topics, and duration; covers both
  agent-planner and mechanical fallback paths
- recall.ts resolveRelevantAutoMemoryPromptForQuery():
  emits recall event with strategy, docs_scanned/selected, and
  duration; covers model, heuristic, and none paths
- forget.ts forgetManagedAutoMemoryEntries():
  emits forget event with removed_entries_count, touched_topics,
  and selection_strategy (model/heuristic/none)
- rememberCommand.ts action():
  emits remember event with topic=managed|legacy at command
  invocation time (before agent decides the actual memory type)

* refactor(telemetry): remove memory forget/remember telemetry events

Remove EVENT_MEMORY_FORGET and EVENT_MEMORY_REMEMBER along with all
associated infrastructure that is no longer needed:

- constants.ts: remove EVENT_MEMORY_FORGET, EVENT_MEMORY_REMEMBER
- types.ts: remove MemoryForgetEvent, MemoryRememberEvent classes
- metrics.ts: remove MEMORY_FORGET_COUNT, MEMORY_REMEMBER_COUNT constants,
  memoryForgetCounter, memoryRememberCounter module vars,
  their initialization in initializeMetrics(), and
  recordMemoryForgetMetrics(), recordMemoryRememberMetrics() functions
- loggers.ts: remove logMemoryForget(), logMemoryRemember() functions
  and their imports
- index.ts: remove all re-exports for the above symbols
- memory/forget.ts: remove logMemoryForget call-site and import
- cli/rememberCommand.ts: remove logMemoryRemember call-sites and import

* change default value

* fix forked agent

* refactor(background): unify fork primitives into runForkedAgent + cleanup

- Merge runForkedQuery into runForkedAgent via TypeScript overloads:
  with cacheSafeParams → GeminiChat single-turn path (ForkedQueryResult)
  without cacheSafeParams → AgentHeadless multi-turn path (ForkedAgentResult)
- Delete forkedQuery.ts; move its test to background/forkedAgent.cache.test.ts
- Remove forkedQuery export from followup/index.ts
- Migrate all callers (suggestionGenerator, speculation, btwCommand, client)
  to import from background/forkedAgent
- Add getFastModel() / setFastModel() to Config; expose in CLI config init
  and ModelDialog / modelCommand
- Remove resolveFastModel() from AppContainer — now delegated to config.getFastModel()
- Strip Claude Code references from code comments

* fix(memory): address wenshao's critical review findings

- dream.ts: writeDreamManualRunToMetadata now persists lastDreamSessionId
  and resets recentSessionIdsSinceDream, preventing auto-dream from firing
  again in the same session after a manual /dream
- config.ts: gate managed auto-memory injection on getManagedAutoMemoryEnabled();
  when disabled, previously saved memories are no longer injected into new sessions
- rememberCommand.ts: remove legacy save_memory branch (tool was removed);
  fall back to submit_prompt directing agent to write to QWEN.md instead
- BuiltinCommandLoader.ts: only register /dream and /forget when managed
  auto-memory is enabled, matching the feature's runtime availability
- forget.ts: return early in forgetManagedAutoMemoryMatches when matches is
  empty, avoiding unnecessary directory scaffolding as a side effect

* fix test

* fix ci test

* feat(memory): align extract/dream agents to Claude Code patterns

- fix(client): move saveCacheSafeParams before early-return paths so
  extract agents always have cache params available (fixes extract never
  triggering in skipNextSpeakerCheck mode)

- feat(extract): add read-only shell tool + memory-scoped write
  permissions; create inline createMemoryScopedAgentConfig() with
  PermissionManager wrapper (isToolEnabled + evaluate) that allows only
  read-only shell commands and write/edit within the auto-memory dir

- feat(extract): align prompt to Claude Code patterns — manifest block
  listing existing files, parallel read-then-write strategy, two-step
  save (memory file then index)

- feat(dream): remove mechanical fallback; runManagedAutoMemoryDream is
  now agent-only and throws without config

- feat(dream): align prompt to Claude Code 4-phase structure
  (Orient/Gather/Consolidate/Prune+Index); add narrow transcript grep,
  relative→absolute date conversion, stale index pruning, index size cap

- fix(permissions): add isToolEnabled() to MemoryScopedPermissionManager
  to prevent TypeError crash in CoreToolScheduler._schedule

- test: update dreamScheduler tests to mock dream.js; replace removed
  mechanical-dedup test with scheduler infrastructure verification

* move doc to design

* refactor(memory): unify extract+dream background task management into MemoryBackgroundTaskHub

- Add memoryTaskHub.ts: single BackgroundTaskRegistry + BackgroundTaskDrainer shared
  by all memory background tasks; exposes listExtractTasks() / listDreamTasks()
  typed query helpers and a unified drain() method
- extractScheduler: ManagedAutoMemoryExtractRuntime accepts hub via constructor
  (defaults to defaultMemoryTaskHub); test factory gets isolated fresh hub
- dreamScheduler: same pattern — sessionScanner + hub injection; BackgroundTask-
  Scheduler initialized from injected hub; test factory gets isolated hub
- status.ts: replace two separate getRegistry() calls with defaultMemoryTaskHub
  typed query methods
- Footer.tsx (useDreamRunning): subscribe to shared registry, filter by
  DREAM_TASK_TYPE so extract tasks do not trigger the dream spinner
- index.ts: re-export memoryTaskHub.ts so defaultMemoryTaskHub/DREAM_TASK_TYPE/
  EXTRACT_TASK_TYPE are available as top-level package exports

* refactor(background): introduce general-purpose BackgroundTaskHub

Replace memory-specific MemoryBackgroundTaskHub with a domain-agnostic
BackgroundTaskHub in the background/ layer. Any future background task
runtime (3rd, 4th, …) plugs in by accepting a hub via constructor
injection — no new infrastructure required.

Changes:
- Add background/taskHub.ts: BackgroundTaskHub (registry + drainer +
  createScheduler() + listByType(taskType, projectRoot?)) and the
  globalBackgroundTaskHub singleton. Zero knowledge of any task type.
- Delete memory/memoryTaskHub.ts: its narrow listExtractTasks /
  listDreamTasks helpers are replaced by the generic listByType() call.
- Move EXTRACT_TASK_TYPE to extractScheduler.ts (owned by the runtime
  that defines it); replace 3 hardcoded string literals with the const.
- Move DREAM_TASK_TYPE to dreamScheduler.ts; use hub.createScheduler()
  instead of manually wiring new BackgroundTaskScheduler(reg, drain).
- status.ts: globalBackgroundTaskHub.listByType(EXTRACT_TASK_TYPE, ...)
- Footer.tsx: globalBackgroundTaskHub.registry (shared, filtered by type)
- index.ts: export background/taskHub.js; drop memory/memoryTaskHub.js

* test(background): add BackgroundTaskHub unit tests and hub isolation checks

- background/taskHub.test.ts (11 tests):
  - createScheduler(): tasks registered via scheduler appear in hub registry;
    multiple calls return distinct scheduler instances
  - listByType(): filters by taskType, filters by projectRoot, returns []
    for unknown types, two types co-exist in registry but stay separated
  - drain(): resolves false on timeout, resolves true when tasks complete,
    resolves true immediately when no tasks in flight
  - isolation: tasks in hubA do not appear in hubB
  - globalBackgroundTaskHub: is a BackgroundTaskHub instance with registry/drainer

- extractScheduler.test.ts (+1 test):
  - factory-created runtimes have isolated registries; tasks in runtimeA
    are invisible to runtimeB; all tasks carry EXTRACT_TASK_TYPE

- dreamScheduler.test.ts (+1 test):
  - factory-created runtimes have isolated registries; tasks in runtimeA
    are invisible to runtimeB; all tasks carry DREAM_TASK_TYPE

* refactor(memory): consolidate all memory state into MemoryManager

Replace BackgroundTaskRegistry/Drainer/Scheduler/Hub helper classes and
module-level globals with a single MemoryManager class owned by Config.

## Changes

### New
- packages/core/src/memory/manager.ts — MemoryManager with:
  - scheduleExtract / scheduleDream (inline queuing + deduplication logic)
  - recall / forget / selectForgetCandidates / forgetMatches
  - getStatus / drain / appendToUserMemory
  - subscribe(listener) compatible with useSyncExternalStore
  - storeWith() atomic record registration (no double-notify)
  - Distinct skippedReason 'scan_throttled' vs 'min_sessions' for dream
- packages/core/src/utils/forkedAgent.ts — pure cache util (moved from background/)
- packages/core/src/utils/sideQuery.ts — pure util (moved from auxiliary/)

### Deleted
- background/taskRegistry, taskDrainer, taskScheduler, taskHub and all tests
- background/forkedAgent (moved to utils/)
- auxiliary/sideQuery (moved to utils/)
- memory/extractScheduler, dreamScheduler, state and all tests

### Modified
- config/config.ts — Config owns MemoryManager instance; getMemoryManager()
- core/client.ts — all memory ops via config.getMemoryManager()
- core/client.test.ts — mock MemoryManager instead of individual modules
- memory/status.ts — accepts MemoryManager param, drops globalBackgroundTaskHub
- index.ts — memory exports reduced from 14 modules to 5 (manager/types/paths/store/const)
- cli/commands/dreamCommand.ts — via config.getMemoryManager()
- cli/commands/forgetCommand.ts — via config.getMemoryManager()
- cli/components/Footer.tsx — useSyncExternalStore replacing setInterval polling
- cli/components/Footer.test.tsx — add getMemoryManager mock
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.

4 participants