mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-26 17:13:32 +00:00
5 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e92fcbab46
|
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some terminals (#5974)
* fix(cli): replace ✦ (U+2726) with ◆ (U+25C6) and add ∵/∴ thinking icons - Replace ✦ with ◆ across all TUI components to fix East Asian Ambiguous width misalignment (string-width reports 1 but terminals render 2 columns). - Use ∵ (because) during thinking streaming, ∴ (therefore) when thinking is complete — matches the mathematical reasoning pair. Co-Authored-By: Qwen Code <noreply@alibaba-inc.com> * fix(cli): reduce STATUS_INDICATOR_WIDTH from 3 to 2 after ◆ replacement ◆ (U+25C6) is a consistent width-1 character across all terminals, so the tool status indicator no longer needs the extra column that was reserved for the ambiguous-width ✦ (U+2726). Co-Authored-By: Qwen Code <noreply@alibaba-inc.com> * fix(cli): catch missed ✦→◆ references in tests, docs, and scenarios * fix(cli): shorten tmux spinner frames from 3 to 2 chars to match STATUS_INDICATOR_WIDTH=2 TMUX_SPINNER_FRAMES changed from ['. ', '.. ', '...'] to ['· ', '··'] to prevent 1-column overflow in tmux when STATUS_INDICATOR_WIDTH was reduced from 3 to 2 after the ◆ replacement. * revert(cli): keep narrow '.' tmux spinner frames instead of ambiguous '·' '.' (U+002E) is Narrow (always 1 col), giving a guaranteed fixed-width tmux spinner. '·' (U+00B7) is East Asian Ambiguous, so on ambiguous-width=2 terminals the frames become 3/4 cols and the spinner jitters — the opposite of the "fixed-width frames" the surrounding comment promises. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen Code <noreply@alibaba-inc.com> Co-authored-by: pomelo.lcw <pomelo.lcw@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
673454839e
|
feat(memory): add a git-shared team memory tier (#5886)
* feat(memory): add a git-shared team memory tier
Add an opt-in third auto-memory tier — TEAM — stored in the repository at
`.qwen/team-memory/` and shared with collaborators through git. The private
USER and PROJECT tiers are unchanged.
- Enabled via the `memory.enableTeamMemory` setting (default off) or the
`QWEN_CODE_MEMORY_TEAM` env override; inactive otherwise.
- The model is taught a three-tier prompt and when to route saves to the
shared tier (project-wide conventions, team-wide references).
- Writes to the team directory are scanned for secrets (a curated
gitleaks-style ruleset) and hard-blocked, since the directory is committed
to the repo and visible to everyone with repo access.
- Team writes default to 'ask' permission — not auto-allowed like the private
tiers — so they are proposed for confirmation in the default approval mode
and surface in the git diff for review.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(vscode): update settings schema for team memory
* fix(memory): harden team memory review gaps
* fix(memory): address team-memory PR review feedback
- secret-scanner: AWS key suffix is base62 ([A-Z0-9]), not base32
([A-Z2-7]) — old class missed IDs with digits 0/1/8/9; add a
positive test for such a key.
- paths: document that isAnyAutoMemPath deliberately excludes team
memory (security-load-bearing — team writes must stay 'ask').
- extractionAgentPlanner: name the team-memory dir in the scoped deny
message so a denied team write is debuggable.
- prompt: spell the tier count via a number-word lookup so a future
4th tier never silently reads "two".
- team-memory-secret-guard: debug-log matched rule IDs only (never the
secret content) when a write is blocked.
- edit: when a blocked team write's secret already exists in the
on-disk file, tell the user to delete the committed secret.
- docs: caveat that a directory-form gitignore (.qwen/) makes the
!-reinclude a no-op; use the file-glob form.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): harden team-memory secret guard on blocked writes and notebooks
- write-file: blocked team-memory secret write now returns an `error`
field (INVALID_TOOL_PARAMS) so the framework treats it as a failure
instead of a silent success, mirroring edit.ts.
- notebook-edit: scan the serialized .ipynb that hits disk through the
team-memory secret guard before writing, blocking with the same
`error` field; behavior unchanged for non-team-memory paths.
- store: log non-ENOENT errors in readTeamAutoMemoryIndex before
re-throwing, so EACCES/EIO/ELOOP no longer vanish via config.ts's
best-effort `.catch(() => null)`.
- config: emit a debug diagnostic when team memory is enabled but
suppressed by the untrusted-workspace trust gate.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* feat(memory): auto-generate the team memory index
Generate the team `MEMORY.md` index by scanning the saved memory files
instead of having the model hand-maintain it. The index is derived and
ordered by path, so the committed file is deterministic across machines.
This removes the git merge-conflict surface a hand-edited shared index would
have, keeps the index consistent with the files, and frees the model from the
two-step save for the team tier.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(memory): opt-in git sync for team memory
Add an opt-in, best-effort git sync of team memory at session start so a
daemon can share team memory through the remote without manual git. Off by
default; enabled with `QWEN_CODE_MEMORY_TEAM_SYNC=1`.
It commits the team directory (only that path), pulls fast-forward-only, then
pushes. `--ff-only` is deliberate: it never creates a merge commit or a
conflict — a diverged branch is skipped rather than touching the working tree.
All git is best-effort and never throws, so a failure cannot break a session.
Commits can be attributed to the acting user via an optional author, for
cooperative per-user attribution on a shared daemon.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): close team-memory secret-guard regex + test gaps
- secret-scanner: add `_` to the two T3BlbkFJ-marker char classes so legacy
base64url OpenAI keys with underscores in the body no longer bypass the
scanner; extend the OpenAI positive samples with an underscore key body.
- edit: cover the pre-existing-secret branch — seed the on-disk file with a
full detectable token, assert the distinct "already exists" message and that
the blocked edit leaves the file untouched.
- team-paths: cover the first-ever write before .qwen/team-memory exists,
exercising realpathNearestExisting walking up to an existing ancestor.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* feat(core): add Google OAuth client secret and SendGrid secret-scanner rules
Extend the team-memory secret scanner with two more high-confidence,
distinctive-prefix credential patterns (both gitleaks rules):
- gcp-oauth-client-secret: GOCSPX- prefix + 24 base64url chars,
placed next to the existing GCP AIza API-key rule.
- sendgrid-api-token: SG. + 22-char id + . + 43-char secret.
Both have a near-zero false-positive bar, matching the curated set's
existing criteria. Add a load-bearing positive sample per rule.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): warn when team memory is enabled but not git-shareable
When the team tier is active, emit a one-time startup warning if its
directory is git-ignored (e.g. a directory-form `.qwen/` ignore that a
`!`-reinclude cannot escape) or there is no git root — so the tier never
silently shares nothing. Gated on the active (trusted + enabled) tier;
probes a representative file path with `git check-ignore`, and is
latched so refreshHierarchicalMemory re-runs do not re-emit it.
Closes the last open review thread (wenshao + qwen-code-ci-bot both
flagged the silent-inert case); the docs caveat already shipped.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* feat(memory): dedup team index by description; fix empty-frontmatter parse
The team index now collapses entries that share a (normalized) description
into one line — listing the other files via "(also: …)" — so two people
saving the same shared fact don't surface as duplicates. Nothing is dropped;
every file path is preserved and the grouping is deterministic (input is
path-sorted).
Also fix a pre-existing frontmatter bug: parseFrontmatterValue used `\s*`,
which crosses newlines, so an empty value (`description:`) captured the next
frontmatter line. Bound it to horizontal whitespace.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* feat(memory): add memory.enableTeamMemorySync setting
Team-memory git sync was env-only (QWEN_CODE_MEMORY_TEAM_SYNC). Add a
first-class `memory.enableTeamMemorySync` setting mirroring the
enableTeamMemory plumbing (settingsSchema, cli/core config, acpAgent
type/schema/default/keys/normalizer, desktop mirror), with the env var kept
as a 0/1 override. Off by default. Docs updated.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): audit hardening — resilient scan, deterministic team index
From a multi-round adversarial audit of the team-memory tier:
- scan: a single unreadable memory file (EACCES, or a TOCTOU delete during
`git pull`) no longer rejects the whole scan and wipe every memory from the
index — per-file failures are caught and skipped (debug-logged).
- indexer: order the team index by code unit, not `localeCompare()`, so the
committed MEMORY.md is byte-identical across machines/locales and the
ff-only sync can't wedge on index churn.
- scan: escape the frontmatter key before building its RegExp (latent hardening).
- config: surface a skipped/failed team-memory sync at debug level instead of
silently swallowing it.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): deterministic team index cap + surface diverged sync
Round-2 audit follow-ups:
- scan: the >200-file cap selected its subset by mtime + locale basename, which
differs per machine/locale — so a large team's committed index churned and
wedged the ff-only sync. Team scans now cap by code-unit path (deterministic);
private tiers keep mtime-recency (never shared, so it's fine).
- sync: a diverged branch made `pull --ff-only` (and the push) fail with no
`skippedReason` — a silent no-op for a user who opted into sync. Now reported
as `pull-failed` / `push-failed` and logged. Docs note pull/push are
whole-branch, not path-scoped.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): regenerate settings schema for enableTeamMemorySync
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): harden team-memory git sync (reconcile-first, scoped push, safe failure)
The opt-in team-memory git sync could publish unrelated local commits, wedge a
two-writer branch, leave team files staged on commit failure, and hang session
start on a credential prompt. Harden all four maintainer-flagged criticals:
- Reconcile first: run `git pull --ff-only` BEFORE committing so the local team
commit lands on top of upstream instead of diverging; a diverged branch is
skipped cleanly as `pull-failed` rather than committing then wedging `--ff-only`.
- Scoped push: push only when THIS sync created the commit and the branch was
not already ahead of `@{u}` (pre-sync), via an explicit single-branch refspec
(`HEAD:<merge-ref>`) — never an unqualified `git push`. New `local-ahead` skip.
- Safe failure: unstage the team path when the commit fails (hook/GPG/identity)
so a user's next manual `git commit` cannot sweep team files in.
- Non-interactive git: force `GIT_TERMINAL_PROMPT=0` + batch-mode SSH, SIGKILL on
timeout, and a shorter timeout so sync cannot block session start on a prompt.
config refresh: rebuild the team index BEFORE syncing (so a fresh MEMORY.md is
committed/pushed, with a re-build after a pull that brought new files) and key
the shareability latch by projectRoot so a newly entered repo is re-checked.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): harden team-memory index/scan/path security
Address the security/correctness criticals from PR #5886 review. Team
memory is committed to the repo and loaded into every collaborator's
system prompt, so its write/scan/classification paths are trust
boundaries.
- indexer: pass noFollow on the team MEMORY.md write and reject a
symlinked team root, so a committed `.qwen/team-memory` (or index)
symlink can't redirect the generated index outside the repo.
- indexer: sanitize attacker-controlled frontmatter title/description
(strip control/zero-width/bidi chars, collapse newlines, defang
code/link markdown, cap length) before embedding into MEMORY.md, to
prevent prompt injection into the shared context.
- indexer: skip the rewrite when the generated index is byte-identical,
avoiding mtime churn and no-op commit cycles.
- scan: normalize CRLF before parsing so a Windows checkout's team files
don't silently drop out of the shared index.
- paths: resolve a dangling final-component symlink via lstat/readlink in
realpathNearestExisting, so a `decoy -> team-memory/leak.md` (missing
target) is classified as team and the secret scanner can't be bypassed.
- secret-scanner: bound both quantifiers in the openai-api-key T3BlbkFJ
rule ({20,512}) to remove O(n^2) ReDoS backtracking; real keys still
match.
- team-memory-git-status: also probe a representative topic path so a
repo that re-includes the index but ignores topic files is flagged as
non-shareable.
- notebook-edit: run the team-memory secret scan in validateToolParamValues
too, for parity with edit/write-file.
Adds load-bearing tests for each fix.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): sanitize team index paths and verify whole-path containment
Refines the earlier team-memory security hardening with two fixes the
first pass left open:
- sanitizeIndexPath now defangs the attacker-controlled relativePath
rendered into the committed MEMORY.md — at BOTH the `](path)` link
target and the "(also: ...)" dedup suffix. A git filename may legally
contain newlines and markdown delimiters, so a raw path could inject a
second physical line (e.g. "- SYSTEM:") and reopen the prompt-injection
structure the patch was closing for title/description.
- rebuildTeamAutoMemoryIndex now realpath-resolves the whole team root
and requires it to equal repoRoot/.qwen/team-memory. The prior lstat
only inspected the leaf, so a symlinked PARENT component (e.g.
`.qwen -> /tmp/out`) let scan/write escape the repo undetected while
the guard believed it had rejected symlinks.
Also adds a regression test pinning the security-load-bearing invariant
that isAnyAutoMemPath excludes team paths (so team writes stay ask-gated).
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): keep team index link targets addressable, not just safe
The earlier injection fix rewrote legal filename chars (`(` `)` `[` `]` and
backticks) to `_` and used that destroyed value as the ACTUAL MEMORY.md link
target. A real file `feedback/a(b).md` was emitted as `](feedback/a_b_.md)`,
which does not exist (or, worse, collides with a different real file) — the
link was injection-safe but no longer addressable.
Replace `sanitizeIndexPath` with `encodeIndexPathTarget`, a reversible
percent-encoder: every char outside an addressable allowlist (alphanumerics
and `/ . - _ ~`) is percent-encoded, so breakout chars become inert ASCII
(newline->%0A, `(`->%28, `)`->%29, space->%20, backtick->%60, ...). The target
is a single line with no `](`/`)` breakout, yet `decodeURIComponent` recovers
the exact original path so the link resolves to the real file. `/` is kept
literal so the path still works as a relative reference. Applied at both index
sites (the main `](path)` link and the `(also: ...)` suffix). The display label
(title) stays non-reversibly sanitized as before — it is not addressable.
Update the path tests to assert the emitted target percent-decodes back to the
real relative path, and add a `feedback/a(b).md` case. The newline+`]`/`)`
injection assertions remain (mutation-verified: they fail if the encoding is
dropped).
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): gate team-memory sync on the index safety check
rebuildTeamAutoMemoryIndex throws when the team root is a symlink that
could redirect the committed index outside the repo, but the surrounding
.catch swallowed the error and syncTeamMemory ran independently — so a
refused symlink/escape still got git add/commit/push'd on the out-of-repo
dir, defeating the indexer's refusal. Capture whether the rebuild
succeeded and only sync when it did, so a failed safety check skips sync
entirely.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): harden team-memory git sync and cover execute-time secret scan
Several robustness fixes to the team-memory git sync, plus a missing test:
- Respect a user's GIT_SSH_COMMAND: append our non-interactive batch guards
(-oBatchMode=yes -oConnectTimeout=5) onto it instead of clobbering it, so a
custom identity (-i), proxy jump (-J), or non-standard port is preserved.
- Skip cleanly on a detached HEAD (new `detached-head` skippedReason) instead
of creating an orphaned commit that can never be pushed. A branch with no
upstream still commits locally as before.
- Use SIGTERM (not SIGKILL) as the git kill signal so a timeout mid-commit/-pull
lets git release index.lock and clean up; the non-interactive ssh guards
already bound the network-hang case.
- docs(memory): correct the sync description to match the implementation
(rebuild index, ff-pull BEFORE commit, push only the scoped sync commit,
skip on divergence/no-upstream).
- test(notebook-edit): cover the execute-time team-memory secret scan (the
full-notebook backstop), distinct from the existing validate-time test.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(memory): split security vs operational team-sync failures; per-op git kill signal
Refine the recent team-memory sync changes per review:
- indexer: throw a typed TeamMemoryRootSecurityError for the symlink/escape
rejection so the sync gate can tell a SECURITY refusal apart from an
OPERATIONAL IO failure. config now blocks sync ONLY on the security class;
an operational rebuild failure (EACCES/ENOSPC/EPERM) is logged but
self-corrects on the next rebuild and no longer permanently gates legitimate
team sync. The security invariant (never add/commit/push an escaping root)
is unchanged.
- team-memory-sync: pass a per-op kill signal to tryGit — SIGKILL for the
hang-prone network/ref ops (pull/push/rev-parse/symbolic-ref; no index.lock
to corrupt), SIGTERM for mutating ops (add/commit) so git can release locks
and clean up. Node's execFile timeout does not escalate to SIGKILL on its
own, so a child trapping SIGTERM could hang past the timeout.
- team-memory-sync: thread the resolved branch into resolvePushTarget so it no
longer re-spawns `git symbolic-ref` after the detached-HEAD check.
- docs: fix a broken sentence in the team-memory git-sync section.
- tests: cover both rebuild failure classes (security -> sync skipped;
operational -> sync still runs) and a positive gate (rebuild ok + sync
enabled -> sync runs); assert the typed error on the indexer symlink tests.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <noreply@qwen.ai>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
|
||
|
|
24ebfbc13e
|
feat(memory): load .qwen/QWEN.local.md as project-local context (#4091) (#4394)
* feat(memory): load .qwen/QWEN.local.md as project-local context (#4091) Adds a per-developer, project-scoped context file slot at `<projectRoot>/.qwen/QWEN.local.md`. Loaded after all hierarchical QWEN.md / AGENTS.md files so local instructions can supplement or override shared ones. Use case: project-specific but personal instructions (local cluster IDs, container registry namespaces, accounts) that shouldn't live in the shared root `QWEN.md` (exposes them to the team) or in the global `~/.qwen/QWEN.md` (applies to every project). Mirrors Claude Code's `.claude/CLAUDE.local.md` convention. The slot is single and fixed (project root only — not searched in CWD subdirectories or via upward traversal), gated by the same trust and explicit-only checks as the rest of project-level discovery, and counted in `fileCount` so the `/memory` panel surfaces it. Users must gitignore the file themselves; `.qwen/` is not auto-ignored and `.qwen/settings.json` is commonly committed. * fix(memory): support .git-file repos when locating QWEN.local.md slot `findProjectRoot()` only accepted `.git` as a directory, so in git worktrees and submodules (where `.git` is a file containing a `gitdir:` pointer) it returned `null`. The new `.qwen/QWEN.local.md` slot then fell back to `<cwd>/.qwen/QWEN.local.md`, silently breaking the documented "single fixed slot at project root" behavior for users inside worktrees — including the developer of this feature. Two changes: 1. `findProjectRoot()` now accepts `.git` as either a directory or a regular file. This also incidentally repairs pre-existing breakage in `rulesDiscovery` / hierarchical-search stop boundary, both of which consume the same helper. 2. The local-context-file slot now requires a real `foundRoot` (the `null` case is no longer covered by the `effectiveRoot` fallback). Without this guard: - a deep cwd in a non-git workspace turned the slot into a per-cwd file, opposite the design; - `cwd === homedir` resolved the slot to `~/.qwen/QWEN.local.md`, colliding with the global Qwen directory. Three regression tests pin the new behavior: `.git`-as-file is recognized, no-`.git`-ancestor skips the slot, `cwd === homedir` without `.git` does not promote a global file to project-local. * refactor(memory): extract findProjectRoot to shared utility (#4091) Two duplicate `findProjectRoot` helpers existed in `packages/core/src/utils/`: one in `memoryDiscovery.ts` (returns `Promise<string | null>`) and one in `memoryImportProcessor.ts` (returns `Promise<string>`, falls back to startDir). The previous fix in 97c6fb41f only updated the first copy for `.git`-file support, so `@import` resolution under git worktrees and submodules was still silently broken — the QWEN.local.md file would load, but its imports would resolve against the wrong root. Extract the helper into `utils/projectRoot.ts`, with the unified nullable return type. Rewire both call sites; `memoryImportProcessor` preserves its previous fallback semantics at the call site (`?? path.resolve(basePath)`). Adds 5 unit tests for the utility (directory / file / null / deep / symlink) and 1 test for the previously-unverified dedup guard in `memoryDiscovery.ts` (exercised via `extensionContextFilePaths`). Addresses inline + cross-file findings from wenshao on PR #4394. |
||
|
|
b004450d7f
|
feat(cli): support multi-line status line output (#3311)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): support multi-line status line output (#3211) Remove the single-line hard limit (.split('\n')[0]) from the status line hook so user scripts can output multiple rows. Footer renders each line as a separate <Text wrap="truncate"> element, preserving per-line horizontal truncation. Ink's virtual DOM handles re-rendering without manual ANSI cursor management. * feat(cli): cap status line output at 3 lines Prevent runaway scripts from flooding the footer — lines beyond the third are silently discarded. * docs: mention 3-line cap in status line docs and agent prompt * fix(cli): cap status line at 2 lines to keep footer within 3 rows Footer has a fixed bottom row (hint/mode indicator), so status line gets at most 2 lines to keep the total footer height at 3 rows max. * test(cli): improve useStatusLine coverage to 100% lines Add tests for: per-model metrics payload, contextWindowSize/version/ model fallbacks, config removal with pending debounce, command change cancelling pending debounce. * docs: update status line ASCII diagram for multi-line layouts Also fix TS error in test (null → null as never for mock return). * refactor(cli): return string[] from useStatusLine, filter empty lines Address review feedback: - Hook returns `lines: string[]` instead of `text: string | null`, eliminating the join/split round-trip with Footer. - Filter empty lines before slicing so leading blanks don't eat real content (e.g. "\n\nreal content" no longer yields ["", ""]). - Export MAX_STATUS_LINES with comment explaining the 3-row constraint. - Use `status-line-${i}` as React key for clarity. * test(cli): add Footer multi-line rendering, \r\n, and pure-newline tests Address remaining review feedback: - Footer test: mock useStatusLine, verify multi-line rendering and hint suppression. - useStatusLine test: add \r\n line ending and pure-newline edge case. * fix(cli): align right footer indicators to top When the status line has multiple rows, the left column becomes taller than the right section. The outer Box defaults to `alignItems: stretch` which caused the indicators to visually center; add `alignItems="flex-start"` on the right Box so they stay anchored to the top row. Reported via e2e test in #3311. |
||
|
|
9e2f63a1ca
|
feat(memory): managed auto-memory and auto-dream system (#3087)
* 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 #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 #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
|