-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat(memory): project-scoped memory writes and .qwen/QWEN.local.md #4290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
83726cf
1fccf10
d6deb95
efca260
fce7655
bfe2476
1bce6c6
0f8ce08
a58cc78
4d7da07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,8 +12,11 @@ import { | |||||||||||||||
| WorkspaceMemoryFileTooLargeError, | ||||||||||||||||
| WorkspaceMemoryWriteTimeoutError, | ||||||||||||||||
| getAllGeminiMdFilenames, | ||||||||||||||||
| getLocalContextFilePath, | ||||||||||||||||
| writeWorkspaceContextFile, | ||||||||||||||||
| type WriteContextFileScope, | ||||||||||||||||
| } from '@qwen-code/qwen-code-core'; | ||||||||||||||||
| import { findProjectRoot } from '@qwen-code/qwen-code-core'; | ||||||||||||||||
| import { writeStderrLine } from '../utils/stdioHelpers.js'; | ||||||||||||||||
| import { isServeDebugMode } from './debugMode.js'; | ||||||||||||||||
| import type { HttpAcpBridge } from './httpAcpBridge.js'; | ||||||||||||||||
|
|
@@ -78,6 +81,13 @@ export interface WorkspaceMemoryRouteDeps { | |||||||||||||||
|
|
||||||||||||||||
| const MAX_MEMORY_CONTENT_BYTES = 1024 * 1024; | ||||||||||||||||
|
|
||||||||||||||||
| const VALID_SCOPES: ReadonlySet<string> = new Set([ | ||||||||||||||||
| 'workspace', | ||||||||||||||||
| 'global', | ||||||||||||||||
| 'local', | ||||||||||||||||
| 'auto', | ||||||||||||||||
| ]); | ||||||||||||||||
|
|
||||||||||||||||
| /** Mount the two memory routes on the supplied Express app. */ | ||||||||||||||||
| export function mountWorkspaceMemoryRoutes( | ||||||||||||||||
| app: Application, | ||||||||||||||||
|
|
@@ -116,9 +126,9 @@ export function mountWorkspaceMemoryRoutes( | |||||||||||||||
| const body = deps.safeBody(req); | ||||||||||||||||
|
|
||||||||||||||||
| const scope = body['scope']; | ||||||||||||||||
| if (scope !== 'workspace' && scope !== 'global') { | ||||||||||||||||
| if (!VALID_SCOPES.has(scope as string)) { | ||||||||||||||||
| res.status(400).json({ | ||||||||||||||||
| error: '`scope` must be "workspace" or "global"', | ||||||||||||||||
| error: `\`scope\` must be one of: ${[...VALID_SCOPES].map((s) => `"${s}"`).join(', ')}`, | ||||||||||||||||
| code: 'invalid_scope', | ||||||||||||||||
| }); | ||||||||||||||||
| return; | ||||||||||||||||
|
|
@@ -183,7 +193,7 @@ export function mountWorkspaceMemoryRoutes( | |||||||||||||||
|
|
||||||||||||||||
| try { | ||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] When Consider defining a typed error class (e.g. — qwen3.7-max via Qwen Code /review |
||||||||||||||||
| scope, | ||||||||||||||||
| scope: scope as WriteContextFileScope, | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The POST handler now accepts Fix: Update — qwen-latest-series-invite-beta-v28 via Qwen Code /review |
||||||||||||||||
| mode, | ||||||||||||||||
| content, | ||||||||||||||||
| projectRoot: deps.boundWorkspace, | ||||||||||||||||
|
|
@@ -204,7 +214,7 @@ export function mountWorkspaceMemoryRoutes( | |||||||||||||||
| deps.bridge.publishWorkspaceEvent({ | ||||||||||||||||
| type: 'memory_changed', | ||||||||||||||||
| data: { | ||||||||||||||||
| scope, | ||||||||||||||||
| scope: result.resolvedScope, | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The synchronous POST response (line ~202) omits — qwen3.7-max via Qwen Code /review |
||||||||||||||||
| filePath: result.filePath, | ||||||||||||||||
| mode, | ||||||||||||||||
| bytesWritten: result.bytesWritten, | ||||||||||||||||
|
|
@@ -337,6 +347,36 @@ async function collectWorkspaceMemoryStatus( | |||||||||||||||
| ); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] No test covers this new — qwen3.7-max via Qwen Code /review |
||||||||||||||||
| files.push(...workspaceFiles); | ||||||||||||||||
|
|
||||||||||||||||
| // Probe for .qwen/QWEN.local.md at the project root (git root, not | ||||||||||||||||
| // necessarily the bound workspace). Uses findProjectRoot to match | ||||||||||||||||
| // the discovery path in memoryDiscovery.ts — in a monorepo where | ||||||||||||||||
| // the daemon is bound to a subdirectory, both must probe the same | ||||||||||||||||
| // directory or writes will silently disappear from the prompt. | ||||||||||||||||
| const resolvedWorkspace = path.resolve(boundWorkspace); | ||||||||||||||||
| const foundProjectRoot = await findProjectRoot(resolvedWorkspace); | ||||||||||||||||
| const effectiveRoot = foundProjectRoot ?? resolvedWorkspace; | ||||||||||||||||
| const localPath = getLocalContextFilePath(effectiveRoot); | ||||||||||||||||
| try { | ||||||||||||||||
| const stat = await fs.stat(localPath); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion]
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||
| if (stat.isFile()) { | ||||||||||||||||
| files.push({ | ||||||||||||||||
| absolutePath: localPath, | ||||||||||||||||
| scope: 'local', | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical]
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||
| bytes: stat.size, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| } catch (err) { | ||||||||||||||||
| if (!isEnoent(err)) { | ||||||||||||||||
| errors.push({ | ||||||||||||||||
| kind: 'memory_file', | ||||||||||||||||
| status: 'error', | ||||||||||||||||
| error: err instanceof Error ? err.message : String(err), | ||||||||||||||||
| errorKind: 'stat_failed', | ||||||||||||||||
| hint: localPath, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| const globalDir = Storage.getGlobalQwenDir(); | ||||||||||||||||
| for (const filename of filenames) { | ||||||||||||||||
| const candidate = path.join(globalDir, filename); | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,10 +4,24 @@ | |
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import * as path from 'node:path'; | ||
| import { QWEN_DIR } from '../utils/paths.js'; | ||
|
|
||
| export const DEFAULT_CONTEXT_FILENAME = 'QWEN.md'; | ||
| export const AGENT_CONTEXT_FILENAME = 'AGENTS.md'; | ||
| export const LOCAL_CONTEXT_FILENAME = 'QWEN.local.md'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Fix: Consolidate — have — qwen-latest-series-invite-beta-v28 via Qwen Code /review |
||
| export const MEMORY_SECTION_HEADER = '## Qwen Added Memories'; | ||
|
|
||
| /** | ||
| * Returns the absolute path to `.qwen/QWEN.local.md` for a given | ||
| * project root. Centralized here so all call sites use a single | ||
| * source of truth — if the directory name or filename changes, only | ||
| * this function needs updating. | ||
| */ | ||
| export function getLocalContextFilePath(projectRoot: string): string { | ||
| return path.join(projectRoot, QWEN_DIR, LOCAL_CONTEXT_FILENAME); | ||
| } | ||
|
|
||
| // This variable will hold the currently configured filename for context files. | ||
| // It defaults to include both QWEN.md and AGENTS.md but can be overridden by setGeminiMdFilename. | ||
| // QWEN.md is first to maintain backward compatibility (used by /init command tool). | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion]
VALID_SCOPESis a runtimeSet<string>, whileWriteContextFileScopeis a compile-time type union. They are maintained independently — adding a scope to one without the other causes silent drift (route rejects type-valid scopes, or resolver falls through to global for unknown scopes).Derive the type from the set:
— qwen-latest-series-invite-beta-v34 via Qwen Code /review