Commit graph

5 commits

Author SHA1 Message Date
pomelo
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>
2026-06-30 22:44:34 +00:00
qqqys
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>
2026-06-27 12:29:48 +00:00
顾盼
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.
2026-05-25 11:22:55 +08:00
Shaojin Wen
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.
2026-04-17 12:44:30 +08:00
顾盼
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
2026-04-16 20:05:45 +08:00