跳到主要内容

数据截至 (上游 commit d87b272aec54)

上下文工程:系统提示、QWEN.md、自动记忆、技能与压缩

30 秒导读: 模型每轮只能看见一个固定大小的窗口。这一章讲 Qwen Code 怎么决定「这一轮往窗口里塞什么」(系统提示 + 项目规约 + 召回的记忆 + 技能清单),以及「窗口快满了扔掉什么」(微压缩、全量压缩、会话落盘)。


1. 这是什么(零基础也能懂)

一句话定义: 上下文工程 = 在有限的上下文窗口里,决定「装什么、装多少、什么时候倒掉」的那一整套工程。

为什么它是个工程问题。 假设你在一个几十万行的仓库里让 AI 改代码。它需要知道的东西远超窗口容量:

它需要知道的从哪来如果全塞会怎样
我是谁、该怎么干活系统提示(内置模板)固定成本,必须装
这个项目的规约QWEN.md / AGENTS.md大项目层层叠加,能吃掉几千 token
用户以前说过的偏好自动记忆目录全塞进去 = 一年后开销爆炸
某类任务的专门流程技能(SKILL.md)几十个技能全展开必然溢出
之前几十轮聊了什么会话历史长会话必然撞窗口上限

Qwen Code 的三招。 这一章就是围绕这三招展开的:

  1. 分层装配 —— 系统提示不是一坨字符串,而是「基座 + 若干可插拔段」按固定顺序拼起来的。
  2. 按需召回 —— 记忆和技能只留一份「索引/清单」常驻,正文等到相关时才拉进来。
  3. 到点压缩 —— 快撞窗口时先做便宜的规则式清理(微压缩),不够再做贵的模型式摘要(压缩)。

一句话直觉: 把上下文窗口当内存,把 QWEN.md、记忆目录、会话 JSONL 当磁盘;系统提示是开机就常驻的内核,记忆召回是按需换页,压缩是内存不够时的 swap。

用起来什么样。 用户几乎察觉不到这一层,只在三处冒头:

$ qwen
> 帮我把 auth 中间件换掉

(后台发生的事,用户看不到:)
· 读到 ~/.qwen/QWEN.md + <repo>/QWEN.md + <repo>/sub/QWEN.md,拼成 userMemory
· 侧查询挑出 2 条相关记忆,包成 <system-reminder> 塞在用户话前面
· 技能清单以 <system-reminder> 增量注入,不动 tools 块

(几十轮之后,用户看得到的:)
· Context left: 22% ← warn 档
· Compressing conversation history… ← auto 档

2. 顶层全景(一次请求里,上下文长什么样)

怎么读这张图: 从上到下就是一次 API 请求的三大块。左列是块名,右框里是这一块由哪些片段拼成、按什么顺序。

┌── system ──────────────────────────────────────────────────────────┐
│ ① 基座提示(Core Mandates / Task Management / Workflows / Tone…) │
│ ② 沙箱段(Seatbelt / 容器 / 无沙箱,三选一) │
│ ③ Actions 段(高风险动作要先问) │
│ ④ Git 段(仅当 cwd 是 git 仓库) │
│ ⑤ 工具调用示例(按模型名选 general / qwen-coder / qwen-vl) │
│ ─────────── 分隔符 "\n\n---\n\n" ─────────── │
│ ⑥ userMemory = QWEN.md 层叠 + .qwen/rules + 「auto memory」提示段 │
│ ─────────── 分隔符 ─────────── │
│ ⑦ appendSystemPrompt(CLI 传入的追加指令) │
│ ⑧ git status(分支 + 近期提交) │
└────────────────────────────────────────────────────────────────────┘
┌── tools ───────────────────────────────────────────────────────────┐
│ 工具声明。Skill 工具的 description 是「静态常量」——刻意不放技能清单 │
└────────────────────────────────────────────────────────────────────┘
┌── messages ────────────────────────────────────────────────────────┐
│ 启动 prelude(环境快照 + 可用技能快照) │
│ …历史轮次…(撞阈值后会被整体换成一个 <state_snapshot> 摘要) │
│ <system-reminder>:本轮召回的记忆 / 技能增量 / plan 模式提醒 │
│ 本轮用户输入 │
└────────────────────────────────────────────────────────────────────┘

部件一句话职责:

部件干什么在哪个文件
系统提示装配拼出上图 ①–⑧packages/core/src/core/prompts.ts
主会话入口决定用内置还是自定义系统提示,末尾贴 git statuspackages/core/src/core/client.ts:803(getMainSessionSystemInstruction)
项目上下文发现逐层向上找 QWEN.md/AGENTS.md,处理 @importpackages/core/src/utils/memoryDiscovery.ts
自动记忆抽取 / 整理 / 召回 / 遗忘,统一入口packages/core/src/memory/manager.ts
技能发现、解析、监听、按路径条件激活packages/core/src/skills/skill-manager.ts
压缩三档阈值 + 模型摘要 + 附件恢复packages/core/src/services/chatCompressionService.ts
微压缩规则式清空老工具结果与图片packages/core/src/services/microcompaction/microcompact.ts
会话落盘JSONL 记录与恢复packages/core/src/services/chatRecordingService.ts

主线走一遍(不进代码): 会话启动时装配 ①–⑧ → 每轮用户输入到来时并行发起「记忆召回」预取 → 组装 <system-reminder> 与用户输入一起发出 → 模型跑完这一轮 → 后台异步跑「记忆抽取」和可能的「做梦」 → 下一轮发送前检查 token 数,够高就先压缩。

主循环本身怎么转见 01 主循环;工具怎么声明与披露见 03 工具层


3. 系统提示装配:一个函数拼出全部固定成本

3.1 它要解决的小问题

系统提示既要覆盖「怎么干活」的全部纪律,又要随环境变化(有没有沙箱、是不是 git 仓库、用的哪个模型),还要允许用户整段替换。写成一个模板字符串会失控,写成一堆 if 拼接会读不懂。

3.2 思路:一个基座 + 若干函数化的段

getCoreSystemPrompt(packages/core/src/core/prompts.ts:110)返回一个模板字符串,里面内联调用了几个返回段落的函数——沙箱段是一个立即执行函数、Git 段是另一个、动作段是 getActionsSection()、示例段是 getToolCallExamples(model)。段落之间没有条件分支缠绕,各自负责判断自己该不该出现。

整段替换的逃生舱。 两个环境变量把这块变成可外部接管的:

环境变量作用代码位置
QWEN_SYSTEM_MD设成路径就从该文件读系统提示;设成 1/true 用默认 .qwen/system.md;设成 0/false 关闭。文件不存在直接抛错prompts.ts:120-135
QWEN_WRITE_SYSTEM_MD把当前基座提示写出到文件,方便 diff 和二次编辑prompts.ts:326-339
QWEN_CODE_TOOL_CALL_STYLE强制选用哪套工具调用示例,绕过按模型名的正则判断prompts.ts:804-819

解析这两个变量的是同一个 resolvePathFromEnv(prompts.ts:19):它先判断值是不是布尔开关,不是就当路径处理并展开 ~

3.3 拼接规则:一个 --- 分隔符管所有追加段

所有「追加到基座后面」的东西走同一个 buildSystemPromptSuffix(prompts.ts:350):

// 示意,非源码
function buildSystemPromptSuffix(text) {
const trimmed = text?.trim();
// 空串/空白 → 返回空,不留下孤零零的分隔符
return trimmed ? `\n\n---\n\n${trimmed}` : '';
}

重点看空值处理:没有内容时返回空串而不是空的分隔块。getCoreSystemPrompt 末尾就是 basePrompt + memorySuffix + appendSuffix(prompts.ts:341-347),getCustomSystemPrompt(prompts.ts:78)走同一个后缀函数——所以「用户自定义系统提示」和「内置系统提示」在记忆追加这件事上行为完全一致。

getCustomSystemPrompt 还多做一件事:把 @google/genaisystemInstruction 联合类型(字符串 / PartUnion[] / Content / 单个 Part)统一压平成文本(prompts.ts:83-102),这样上游不管以哪种形态传自定义提示,下游都只看到字符串。

3.4 三套工具调用示例,按模型名选

getToolCallExamples(model)(prompts.ts:802)在三份常量里选一份:

常量触发条件(正则)定义处
qwenCoderToolCallExamples/qwen[^-]*-coder/i/coder-model/iprompts.ts:546
qwenVlToolCallExamples/qwen[^-]*-vl/iprompts.ts:703
generalToolCallExamples兜底prompts.ts:470

判断前有一行 model.length < 100 的长度护栏,避免超长模型名喂给正则。这是「同一份系统提示按后端模型微调示例风格」的做法;多协议模型层本身见 02 多协议模型层

3.5 两个不进主提示的专用提示

它们不属于主循环的系统提示,但同属「上下文工程」的产物:

  • getCompressionPrompt()(prompts.ts:392)—— 压缩用的系统提示。要求摘要模型先在 <analysis> 里打草稿,再吐一个 9 段的 <state_snapshot> XML(primary_request_and_intent / key_technical_concepts / files_and_code_sections / errors_and_fixes / problem_solving / all_user_messages / pending_tasks / current_work / next_step)。注意它不含"别跟用户打招呼"那句收尾指令——那句由 postProcessSummary 单独贴,理由见 §7.3。
  • getProjectSummaryPrompt()(prompts.ts:445)—— 生成落盘的项目摘要 markdown(Overall Goal / Key Knowledge / Recent Actions / Current Plan),供跨会话参考。

4. 项目上下文文件:QWEN.md 的层叠与 @import

4.1 它要解决的小问题

项目规约需要能分层:全局偏好写在 ~/.qwen/QWEN.md,仓库规约写在仓库根,子目录规约写在子目录,个人私货写在一个 gitignore 掉的文件里——而且要兼容业界的 AGENTS.md

4.2 四个文件名,各司其职

packages/core/src/memory/const.ts 定义了全部命名:

常量语义
DEFAULT_CONTEXT_FILENAMEQWEN.md主上下文文件const.ts:7
AGENT_CONTEXT_FILENAMEAGENTS.md兼容名,与 QWEN.md 并列参与逐层搜索const.ts:8
LOCAL_CONTEXT_FILENAMEQWEN.local.md个人私有槽位,不参与逐层搜索const.ts:28
MEMORY_SECTION_HEADER## Qwen Added Memories/memory add 追加内容的锚点const.ts:29

默认的文件名列表是 [QWEN.md, AGENTS.md],QWEN.md 在前是为了 /init 的向后兼容。setGeminiMdFilename(const.ts:39)允许覆盖;getCurrentGeminiMdFilename(const.ts:49)在数组里挑第一个非空项——注释里记了一段真实事故:daemon 侧的 extractContextFilename 会跳过空项而这里当初不会,于是父进程写 AGENTS.md、子进程读 '',/init 出来的文件成了孤儿。

packages/core/src/tools/memory-config.ts 只是把这些常量再导出一遍(全文 19 行),目的是让只要文件名的调用方不必加载整个 memoryTool 模块——一个很小但很典型的加载成本优化。

4.3 分层发现的顺序

getGeminiMdFilePathsInternalForEachDir(packages/core/src/utils/memoryDiscovery.ts:94)对每个候选目录做这件事:

┌─────────────────────────────┐
对每个文件名 │ QWEN.md,然后 AGENTS.md │
(外层循环) └──────────────┬──────────────┘

① 全局:~/.qwen/<文件名> ← 总是先加

┌──────────────────┴──────────────────┐
cwd 是家目录 cwd 是普通目录且 folderTrust
│ │
② 只看 ~/<文件名> ③ 从 cwd 逐层向上找到「项目根的上一层」
用 unshift 保证「越靠外越先」的顺序


④ 扩展提供的上下文文件路径(extensionContextFilePaths)


⑤ <projectRoot>/.qwen/QWEN.local.md(单一固定槽位,最后加载 → 可覆盖共享规约)

第 ③ 步的向上扫描有两个停止条件(memoryDiscovery.ts:173-196):撞到 ~/.qwen 目录就 break;走到 ultimateStopDir(项目根的父目录,没有项目根则是家目录的父目录)就停。项目根的判定是「最近的含 .git 目录 .git 文件的祖先」——后者覆盖 worktree 和 submodule。

第 ⑤ 步的守卫值得单看(memoryDiscovery.ts:508):本地槽位要求真实的 foundRoot,不接受「cwd 兜底」。注释写清了两个反例——非 git 工作区里深 cwd 会让「单一槽位」退化成「每个 cwd 一个」;cwd === homedir 会把槽位解析到 ~/.qwen/QWEN.local.md,和全局目录撞车。

拼接时每份文件都被包上路径标记(memoryDiscovery.ts:354concatenateInstructions):

--- Context from: sub/QWEN.md ---
(内容)
--- End of Context from: sub/QWEN.md ---

另外 createMemoryTypeClassifier(memoryDiscovery.ts:407)会给每份文件打上 extension / user / local / project 标签,用于钩子事件通知——钩子体系见 04 安全护栏

4.4 @import:把上下文文件拆成小块

processImports(packages/core/src/utils/memoryImportProcessor.ts:209)支持 @path/to/file 语法。四个设计点:

关切做法位置
别把代码块里的 @ 当导入先用 marked 词法分析拿到全部 code / codespan 区间,命中就原样保留memoryImportProcessor.ts:165(findCodeRegions)
别无限递归maxDepth: 5,processedFiles 集合防环,重复文件写成 <!-- File already processed -->memoryImportProcessor.ts:228:366
别被路径穿越validateImportPath 拒绝 URL,并要求解析后的路径落在项目根之内memoryImportProcessor.ts:424
两种输出形态tree<!-- Imported from: x --> 内联包裹;flat 把所有文件去重后按 --- File: x --- 平铺memoryImportProcessor.ts:239:343

一个细节很讲究:找导入用的是手写扫描而非正则(findImports,memoryImportProcessor.ts:89)——要求 @ 前是空白(词边界),@ 后第一个字符是 . / / 或字母。文件不存在时原样保留 @path 文本,因为它多半根本不是导入,只是一句提到 @ 的散文。

4.5 装配进系统提示

Config.refreshHierarchicalMemory(packages/core/src/config/config.ts:2360)调 loadServerHierarchicalMemory 拿到 memoryContent,再把 .qwen/rules/ 的基线规则接在后面,最后(当托管记忆可用时)用 memoryManager.appendToUserMemory(...)(config.ts:2471)把「auto memory」提示段追加上去,setUserMemory 存起来。这个字符串就是 §2 图里的 ⑥。


5. 自动记忆:一个跨会话的闭环

5.1 它要解决的小问题

QWEN.md人写的规约。但「用户上周说过测试不许 mock 数据库」这种事,希望是agent 自己记下来、下次自己想起来。难点在于:写多了会膨胀、写重了会矛盾、全塞进上下文会破产。

5.2 闭环全景

怎么读这张图: 竖线是一次会话的时间轴,横向分叉是三个独立触发的动作。

用户输入

├──(A) 召回 recall ──────────────────────────────────┐
│ 并行预取,不阻塞主请求 │
▼ │
组装本轮请求 ◀── 已 settle 就把 ≤5 篇记忆包成 ────────────┘
│ <system-reminder> 插进去

主循环跑完这一轮

├──(B) 抽取 extract ──> fork 子代理读「本轮新增对话」
│ ──> 写 memory/<type>/xx.md
│ ──> 重建 MEMORY.md 索引

└──(C) 做梦 dream(距上次 ≥24h 且期间 ≥5 个会话)
──> fork 子代理去重、合并、修正、剪枝

三层存储互不混淆:

目录谁能看见解析函数
project<runtimeDir>/projects/<hash>/memory/只有你、只在本项目getAutoMemoryRoot(packages/core/src/memory/paths.ts)
user~/.qwen/memories/只有你、跨所有项目getUserAutoMemoryRoot
team<gitRoot>/.qwen/team-memory/仓库所有协作者(走 git 同步)getTeamAutoMemoryRoot

每层内部按四个 topic 分子目录:user / feedback / project / reference(packages/core/src/memory/types.ts:7AUTO_MEMORY_TYPES),每条记忆一个 .md 文件,带 name / description / type 三个 frontmatter 字段。每层各有一个 MEMORY.md 索引,只放一行一条的指针,不放正文。

5.3 记忆文件长什么样

packages/core/src/memory/prompt.ts:16MEMORY_FRONTMATTER_EXAMPLE 就是教模型写的模板:

---
name: {{memory name}}
description: {{one-line description — used to decide relevance in future conversations, so be specific}}
type: {{user, feedback, project, reference}}
---

{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines}}

description给未来的召回用的——召回时模型只看得到 header(type、路径、时间、description),看不到正文,所以 description 写得糊等于这条记忆以后再也捞不出来。

四种 topic 的语义与「该存哪一层」写在 TYPES_SECTION_INDIVIDUAL(prompt.ts:28)里,每种带一个 <scope> 标签:

topicscope一句话
user永远 user 层用户是谁、什么背景、什么偏好
feedback默认 user;只有当它是全项目公约时才落 project用户纠正过 / 确认过的做事方式(成功也要记,只记纠正会越来越保守)
project永远 project 层本项目的目标、事故、截止日期(要求把相对日期转绝对日期)
reference默认 project外部系统指针(Linear 项目、Grafana 面板、Slack 频道)

同一文件里的 WHAT_NOT_TO_SAVE_SECTION(prompt.ts:99)反过来列了不许记的:代码结构与文件路径(读代码就有)、git 历史(git log 权威)、调试修复配方(修复在代码里)、MCP 工具的参数 schema(活的工具定义才权威)、已经写在 QWEN.md 里的东西、临时任务态。最后一句很关键——这些排除项即使用户明确要求也照样适用

5.4 抽取:交给一个受限的分身

runAutoMemoryExtract(packages/core/src/memory/extract.ts:102)本身不调模型,它做的是游标管理:

// 示意,非源码
const cursor = await readExtractCursor(projectRoot);
// 同一会话才复用偏移;换会话从 0 开始
let start = cursor.sessionId === sessionId ? (cursor.processedOffset ?? 0) : 0;
// 历史可能被压缩缩短过,越界就重置,否则新消息永远被跳过
if (start > history.length) start = 0;

// 只在未处理的那一小段里找「非空的用户消息」
const hasNew = history.slice(start).some(m => m.role === 'user' && ...);
if (!hasNew) { await writeCursor(...); return { touchedTopics: [] }; }

重点看 start > history.length 那一行:压缩会让历史变短,不夹这一下的话游标会永远停在未来,新消息全被吞掉。

真正干活的是 runAutoMemoryExtractionByAgent(packages/core/src/memory/extractionAgentPlanner.ts:246),它用 runForkedAgent(packages/core/src/utils/forkedAgent.ts:430)起一个分身:共享父会话的历史(走 getCacheSafeParams() 拿缓存安全的历史),但换一套系统提示、换一套工具、换一套权限。

权限是这里最讲究的部分(extractionAgentPlanner.ts:114createMemoryScopedAgentConfig):

工具分身的判定依据
run_shell_command用 AST 判断是否只读,非只读一律 denyextractionAgentPlanner.ts:78-86
edit / write_file路径必须落在 project 或 user 记忆根内,否则 denyextractionAgentPlanner.ts:87-92
其他交回基础权限管理器evaluateScopedDecisiondefault 分支

分身的决定和父配置的决定用 mergePermissionDecision(extractionAgentPlanner.ts:59)按 deny > ask > allow > default 取严者。team 记忆刻意不在放行名单里——它会被提交进仓库,写它必须保持 ask,isAnyAutoMemPath(packages/core/src/memory/paths.ts)的注释把这条标成 security-load-bearing。

预算是硬的:maxTurns 默认 5、maxTimeMinutes 默认 2(均可经配置覆盖,extractionAgentPlanner.ts:276-277)。任务提示里因此明确教了省轮次的打法——先并行发所有 read,再并行发所有 write,别交替(extractionAgentPlanner.ts:157)。

分身写了什么,靠路径反推(touchedTopicsFromFilePaths,extractionAgentPlanner.ts:307),不要求模型返回 JSON。这个前缀匹配也踩过坑:Windows 上根是反斜杠而模型回参常是正斜杠,所以两边先统一成 /;并且要求根之后紧跟的字符必须是 /,避免 /foo/memory 误匹配 /foo/memory-other/...

补充一句考古:packages/core/src/memory/extractionPlanner.ts 现在是一个 11 行的空壳,注释写着「抽取不再有独立的模型规划阶段,已并入 forked agent 路径」。

5.5 做梦:离线整理

runManagedAutoMemoryDream(packages/core/src/memory/dream.ts:60)同样是一层薄壳,真活在 planManagedAutoMemoryDreamByAgent(packages/core/src/memory/dreamAgentPlanner.ts:102)。相比抽取:

  • 预算更大:MAX_TURNS = 8MAX_TIME_MINUTES = 5(dreamAgentPlanner.ts:32-33)。
  • 只能写 project 层(isAutoMemPath,不含 user 层)。
  • 任务提示是四阶段的(buildConsolidationTaskPrompt,dreamAgentPlanner.ts:48):Orient 摸清现状 → Gather 从会话 JSONL 里窄条件 grep(明确写了「别整篇读」)→ Consolidate 合并重复、修正过期、把「昨天」这类相对日期转成绝对日期 → Prune 重建索引并压到 200 行 / 25KB 以内。

取消语义处理得很细:runForkedAgent 把取消映射成 status: 'cancelled'正常 resolve,dreamAgentPlanner.ts:138-147 特意把它转成 throw,免得上层把「被用户取消」当成「跑完了」,从而错误地写下调度元数据。dream.ts:90 又补一刀——已 abort 就直接返回,不做昂贵的索引重建。

5.6 召回:先问模型,再退回启发式

resolveRelevantAutoMemoryPromptForQuery(packages/core/src/memory/recall.ts:401)是两级策略:

扫描 project + user 两层的所有记忆文件(user 层失败只 warn,不影响 project)


有 config?──否──> 启发式打分
│是

模型选择器 selectRelevantAutoMemoryDocumentsByModel
(侧查询,temperature=0,结构化输出,只看 header 不看正文)

┌────────┴────────┐
成功 抛异常
│ │
strategy=model 是调用方主动 abort?──是──> 直接返回空(结果反正会被丢)
│否(30s 安全网超时 / 真实模型错误)

启发式打分 → strategy=heuristic

启发式打分(recall.ts:220scoreDocument)很朴素:query 分词后每命中一个 token +2,命中 topic 关键词表 +1,正文非空 +1。

两个细节值得抄:

其一,选择器 manifest 用绝对路径而非相对路径(packages/core/src/memory/relevanceSelector.ts:54 formatMemoryManifest)。project 层和 user 层都可能有 user/role.md,用相对路径做 Map 的 key 会静默丢掉一个 scope。

其二,新鲜度是用自然语言表达的(packages/core/src/memory/memoryAge.ts):

// 示意,非源码
memoryAge(mtimeMs) // → 'today' / 'yesterday' / '47 days ago'

注释解释了为什么不给 ISO 时间戳:模型不擅长日期算术,一个裸时间戳触发不了"这可能过期了"的推理,而"47 days ago"能(memoryAge.ts:16-20)。超过 1 天的记忆还会附一段 memoryFreshnessText——明说"记忆是时间点观测不是实时状态,代码行为和 file:line 引用可能已失效,断言前先核对"。

召回结果最后由 buildRelevantAutoMemoryPrompt(recall.ts:327)渲染,单篇正文截到 1200 字符(MAX_DOC_BODY_CHARS),最多 5 篇(MAX_RELEVANT_DOCS)。

还有一条反噪音规则:如果本轮刚用过某个工具,那些「讲这个工具怎么调用」的记忆会被剔掉(createActiveToolUsageFilter,recall.ts:186)——活的工具定义才是权威。但「凭证放哪、谁负责、已知坑」这类从 schema 里得不到的运维知识不剔(DURABLE_ACTIVE_TOOL_MEMORY_MARKERS)。

5.7 索引重建、敏感信息拦截、遗忘

索引rebuildManagedAutoMemoryIndex(packages/core/src/memory/indexer.ts:236)扫盘重建,每条一行 - [Title](path) — hook,整体压到 200 行 / 25KB。

team 层的索引(rebuildTeamAutoMemoryIndex,indexer.ts:290)多扛四件事,因为它会被 commit 进仓库、进每个协作者的系统提示:

风险防线位置
frontmatter 里藏提示注入sanitizeIndexField 去控制字符/零宽/双向覆写字符,反引号换成单引号,]( 拆开,截 120 字符indexer.ts:49
文件名里藏换行/括号导致链接越界encodeIndexPathTarget 百分号编码——既堵住越界,decodeURIComponent 又能还原真实路径(早期版本改成 _,防住了注入但链接指向不存在的文件)indexer.ts:89
目录被 symlink 重定向到仓库外lstat 查叶子,再 realpath 整条链并要求等于仓库内的字面位置;失败抛 TeamMemoryRootSecurityError(与运维类错误刻意区分,只有它能阻断 git 同步)indexer.ts:290-327
多人来回改动引发 no-op commit 乒乓排序用码元比较而非 localeCompare(跨机器字节一致);内容一字不差就跳过重写indexer.ts:329-346

敏感信息拦截checkTeamMemorySecrets(packages/core/src/memory/team-memory-secret-guard.ts:23)在写入前把关:先做便宜的路径判断,不是 team 路径直接放行;是的话调 scanForSecrets(packages/core/src/memory/secret-scanner.ts:167)。规则是 gitleaks 的一个精选子集(AWS / 阿里云 / GCP / Anthropic / OpenAI / GitHub / GitLab / Slack / SendGrid / npm / Stripe / PEM 私钥),只挑「有独特前缀、误报接近零」的那些。匹配到的值从不返回、从不记日志,只返回规则 ID 和标签。

顺带一个 ReDoS 的记录:openai-api-key 规则把 T3BlbkFJ 字面量两侧的量词从 {20,} 收成 {20,512}——两个无界量词夹一个可能不存在的字面量,构造输入能打出 O(n²) 回溯(secret-scanner.ts:70-78)。

路径归类那侧也有一处安全设计:isTeamAutoMemPathrealpathNearestExisting,而它会先 resolveLeafSymlink 跟随悬空 symlink(packages/core/src/memory/paths.ts)。注释写明了攻击:fs.existsSync 会跟随链接、把悬空链接报成"不存在",于是攻击者预置 decoy.md -> .qwen/team-memory/leak.md(目标尚不存在),路径就被判成 team 目录之外、跳过密钥扫描——而真正的写入却顺着链接落进了 team 目录。

遗忘 是两步的(packages/core/src/memory/forget.ts):selectManagedAutoMemoryForgetCandidates(:194)用侧查询挑候选,forgetManagedAutoMemoryMatches(:237)执行删除,forgetManagedAutoMemoryEntries(:326)是二合一的便捷入口。拆两步是为了能在中间插一个用户确认。

5.8 MemoryManager:唯一入口

packages/core/src/memory/manager.ts:411MemoryManager 把上面全部收口成一个稳定 API:

方法干什么
scheduleExtract排一次抽取;同项目已在跑就排队并用新参数顶掉旧的排队请求manager.ts:599
scheduleDream过一串闸门后排一次做梦manager.ts:960
recall转发到 resolveRelevantAutoMemoryPromptForQuerymanager.ts:1385
forget / selectForgetCandidates / forgetMatches遗忘的三个入口manager.ts:1396-1421
getStatus/memory 面板的状态快照manager.ts:1426
drain等所有在飞任务落地(可带超时),退出前用manager.ts:568
appendToUserMemory把 auto memory 提示段贴到 userMemory 尾巴上manager.ts:1438

抽取的第一道闸门很有意思(manager.ts:604):如果主 agent 这一轮自己写了记忆文件(historyWritesToMemory 扫历史里的 write_file/edit/replace/create_file 调用参数),就直接跳过抽取,理由记为 memory_tool。用户刚亲口让 agent 记的东西,没必要再让分身抽一遍。

做梦的闸门是一串(manager.ts:968-1049),任何一道不过就返回一个带 skippedReason 的跳过结果:

闸门跳过理由
功能未开 / 无 configdisabled
运行时内存压力 hard/criticalmemory_pressure
本会话已经做过梦same_session
距上次不足 24h(DEFAULT_AUTO_DREAM_MIN_HOURS,manager.ts:218)min_hours
距上次文件系统扫描不足 10 分钟scan_throttled
期间会话数不足 5(DEFAULT_AUTO_DREAM_MIN_SESSIONS,manager.ts:219)min_sessions
锁文件存在locked
同项目已有在飞任务running

锁不是简单的文件存在判断(dreamLockExists,manager.ts:365):锁里写的是持有者 PID,读出来先 process.kill(pid, 0) 探活;进程没了或者锁超过 1 小时就直接清掉。还有一个 dreamLockReleaseFailed 标志(manager.ts:458)——释放锁失败(Windows EPERM、磁盘满)会留下一个「新鲜 mtime + 活着的 PID(就是我们自己)」的锁,那会把后续每一次调度都静默压制掉;标志让下一次调度先强删再检查,本会话内就能自愈。

5.9 什么时候触发:client.ts 里的两个挂载点

召回是预取的(packages/core/src/core/client.ts:2009)。UserQuery/Cron 一进来就发起 recall,不等它,把 handle 存进 pendingMemoryPrefetch。消费有两个机会点:

  1. UserQuery 组装请求前做一次零等待轮询(client.ts:2287)——只有已经 settle 才取,取到就 unshift<system-reminder> 最前面(贴近用户输入)。
  2. 还没 settle 的话,下一个 ToolResult 轮次再试(client.ts:2301)——这次是 push 到末尾,因为 ToolResult 轮次开头必须是紧跟 functionCall 的 functionResponse,插在前面会被 API 拒。

父轮次的 abort 信号会桥接进预取的 controller(client.ts:1993-1998),Ctrl-C 能一起掐掉;{ once: true } 加上 promise.finally 里的 removeEventListener,两条路都不漏监听器。

抽取与做梦是后置的(runManagedAutoMemoryBackgroundTasks,client.ts:1552)。三条规则:

  • shutdownRequested 时全部跳过,不给退出添新活。
  • 技能复盘(auto-skill)在 UserQuery ToolResult 上都可能触发,因为它按工具调用计数(阈值 AUTO_SKILL_THRESHOLD = 20)走,得能在会话中间点火。
  • 抽取和做梦只在 UserQuery 上触发,保持「每个用户轮次至多一次」的语义。

6. 技能:把「专门流程」按需拉进上下文

6.1 它要解决的小问题

有些任务有固定套路(做 PDF、建新项目、跑 code review)。把套路全写进系统提示,等于让每次请求都为所有套路付钱。

6.2 磁盘布局与多级发现

一个技能 = 一个目录 + 一个 SKILL.md。发现是「级别 × 提供方目录」的二维扫描(packages/core/src/skills/skill-manager.ts:407refreshCache):

级别目录说明
project<projectRoot>/{.qwen,.agents}/skills/项目根等于家目录时跳过,避免和全局撞车
user~/.qwen/skills/~/.agents/skills/
extension由激活的扩展提供不走目录扫描
bundled随包发布的 dist/bundled/safe mode 下加载这一层

提供方目录列表是 SKILL_PROVIDER_CONFIG_DIRS = ['.qwen', '.agents'](packages/core/src/config/storage.ts:17),顺序即优先级:先出现的目录里的同名技能赢(skill-manager.ts:1006-1018)。四个级别之间也是先到先得,project > user > extension > bundled

四个级别用 Promise.allSettled 并行(skill-manager.ts:422)——一个级别的目录权限炸了不该带走另外三个。

符号链接是刻意允许指向外部的。 validateSymlinkTarget(packages/core/src/skills/symlinkScope.ts:48)只检查「能解析」且「是目录」,不做包含性检查。symlinkScope.ts:18-31 记了这个决定的来龙去脉:曾经加过越界拒绝,但威胁模型只在「攻击者能写 symlink 却不能写普通文件」时成立——在单用户的 ~/.qwen/skills/ 上这几乎不可能(能写目录就能直接放个真 SKILL.md),而它却打断了「一个技能仓库 + 若干 symlink」这个被支持的工作流,所以净收益为负,撤了。

6.3 文件监听:depth 是关键

startWatching(skill-manager.ts:562)用 chokidar 监听 project 和 user 两级目录(bundled 不可变、extension 由扩展系统管,都不监听)。核心是一个常量:

// packages/core/src/skills/skill-manager.ts:59
export const WATCHER_MAX_DEPTH = 2;

技能布局是固定的 <skill-name>/SKILL.md,深度 2 足够察觉任何变化。不限深会让 chokidar 爬进 node_modules 之类的重子树,把文件描述符耗尽(注释指向 issue #3289)。配套的 watcherIgnored(skill-manager.ts:63)挡掉 socket / FIFO / 设备文件(监听它们会 EOPNOTSUPP)和 .git

变更事件不直接重扫,走 scheduleRefresh(skill-manager.ts:1193)做 debounce,refresh 完再 updateWatchersFromCache() 把新出现的目录纳入监听。

6.4 解析与激活

parseSkillContent(skill-manager.ts:652)用 /^---\n([\s\S]*?)\n---(?:\n|$)([\s\S]*)$/ 切 frontmatter 和正文,YAML 解析后要求 namedescription 非空。解析失败进 parseErrors Map,由 /skills 面板展示——不抛给上层,一个坏技能不该拖垮整层。

条件激活 是这一节最像「上下文工程」的部分。带 paths: frontmatter 的技能叫条件技能,默认不出现在清单里;只有当某次工具调用碰到匹配 glob 的文件,它才被激活并加入清单:

// 示意,非源码
// 每次工具调用后,把涉及的文件路径喂给注册表
const newlyActivated = registry.matchAndConsume('src/api/handler.ts');
// → ['api-conventions'] 这个技能从此在本次会话里常驻清单

实现在 SkillActivationRegistry(packages/core/src/skills/skill-activation.ts:80),splitConditionalSkills(:42)负责分流。三个细节:

  • glob 编译用 picomatch(p, { dot: true })(skill-activation.ts:100)——激活问的是「模型碰没碰这个文件」,gitignore 那套「跳过隐藏文件」的语义在这里是错的,.eslintrc.js 也该触发。
  • 单个 glob 编译失败只丢那一条 pattern、保住技能其余部分,并通过 onInvalidPattern 回调上报到 parseErrors(skill-activation.ts:101-117)——这个调用点在 Promise.allSettled 的保护圈之外,让它抛会整个中止技能加载。
  • 激活状态不跨 refresh 继承:每次 refreshCache 都新建注册表,这样编辑 paths: 能立刻生效。

6.5 最妙的一点:技能清单不放在工具描述里

SKILL_TOOL_DESCRIPTION(packages/core/src/tools/skill.ts:51)是一个静态常量,里面只有「怎么调用技能」的说明,没有任何技能名字。真正的清单以 <available_skills><system-reminder> 形式,在启动 prelude 里给一份快照,之后每轮只发增量。

理由写在注释里(skill.ts:41-49:161-166):tools 块位于 tools → system → messages 这条 prompt cache 前缀的最前面。把技能清单放进工具描述,意味着任何一次技能增删都会改动前缀首字节,整段缓存作废。配套地,SkillTool.refreshSkills() 只更新内存里的校验集合,绝不调 setTools()

这是「上下文工程」和「成本工程」的交汇点——同一份信息,换个位置放,缓存命中率完全不同。

技能正文是调用时才读的(loadSkillForRuntime,skill-manager.ts:375),随后 buildSkillLlmContent(packages/core/src/tools/skill-utils.ts:18)在正文前贴一行技能基目录,并叮嘱「所有相对路径都从这里解析」。内置技能在 packages/core/src/skills/bundled/:batchextension-creatorloopnew-appqc-helperreviewsimplifystuck


7. 压缩与会话:窗口满了怎么办

7.1 三档阈值:比例与绝对值取大

computeThresholds(packages/core/src/services/chatCompressionService.ts:200)是个纯函数,给定窗口大小算出三个刻度:

effectiveWindow = window - SUMMARY_RESERVE(20_000)

auto = max( pct × window , effectiveWindow - 13_000 )
warn = max( 0, max((pct-0.1) × window , auto - 20_000) )
hard = min( window, max(effectiveWindow - 3_000, auto + 3_000) )
↑ 保证 hard ≥ auto

pct 默认 0.7。每档都是「比例分支」和「绝对分支」取大:小窗口下绝对分支会变成负数,自动退回比例分支;大窗口下绝对分支占优,把浪费的预留从「窗口的 30%」压到「约 33K」这个固定量(chatCompressionService.ts:185-198)。

三档的含义:

触发什么
warnUI 提示"上下文快满了"
auto自动压缩
hard强制压缩,绕过连续失败熔断器

熔断器:连续失败 MAX_CONSECUTIVE_FAILURES = 3 次后,非强制路径直接 NOOP,直到一次成功的强制压缩把计数清零(chatCompressionService.ts:380-389geminiChat.ts:2146-2163)。压不动的会话不该每次发送都白烧一次压缩 API 调用。

还有一个非 token 触发器:即使 token 没到阈值,只要工具返回的图片累计超过 screenshotTriggerThreshold(默认 50),也压一次(chatCompressionService.ts:428-444)。computer-use 类会话不该被陈年截图淹死。这一档触发时 triggerReason 会标成 image_overflow,好让 UI 提示说人话而不是谎称"接近 token 上限"。

7.2 压缩怎么跑

ChatCompressionService.compress(chatCompressionService.ts:352)的主线:

便宜的闸门(熔断器 / 阈值 / 历史 < 2 条)


PreCompact 钩子(可返回 additionalContext,截到 4000 字符)


slimCompactionInput:把 inlineData 换成占位符
(原始带图历史另行保留,供压缩后恢复)


runSideQuery(stream=true, maxAttempts=1, thinking 关闭,
maxOutputTokens=20_000, system=getCompressionPrompt()+附加指令)


判空:看的是「剥掉 <analysis> 之后」的摘要


composePostCompactHistory:摘要 + 模型 ack + 近期文件 + 近期图片

四处刻意为之:

  • 不用 getHistory(true) 深拷贝,用 getHistoryShallow(true)(chatCompressionService.ts:447-452)。压缩正是内存吃紧的时刻,防御性深拷贝可能比剩余堆空间还大。
  • stream: true(chatCompressionService.ts:537-542)。非流式在整段摘要生成完前不吐任何字节,过 BFF 网关时会被短 proxy_read_timeout 打成 504(表现为 422),压缩中途炸掉会话(issue #5861)。
  • maxAttempts: 1(:505)。失败就 NOOP,下一轮自然会重试,不该阻塞用户去烧 7 次重试。
  • 判空判的是剥离后的文本(:540-541)。如果模型只吐了 <analysis> 没吐 <state_snapshot>,原文非空但剥完是空——按原文判会「压缩成功」而 agent 只剩 [Summary unavailable],指标全绿、记忆全失。

输出长度撞到 COMPACT_MAX_OUTPUT_TOKENS 也算失败(:584-611),单独给一个 COMPRESSION_FAILED_OUTPUT_TRUNCATED 状态,好让遥测把「提示质量问题(摘要为空)」和「容量问题(撞上限)」分开看。CompressionStatus 全集在 packages/core/src/core/turn.ts:235

7.3 压缩后的历史长什么样

composePostCompactHistory(packages/core/src/services/postCompactAttachments.ts:772)产出:

user : postProcessSummary(summary) ← 剥掉 <analysis> + 贴 RESUME_TRAILER
model : "Got it. Thanks for the additional context!"
user : [plan 模式提醒] [后台子代理快照] [近期文件正文 ≤5] [近期图片 ≤3]
model : (若有)悬着的 functionCall

三个必须绕开的坑:

处理
连续同角色消息会被 API 拒全部附件合并进一条 user Content(postCompactAttachments.ts:795-817)
悬着的 functionCall 没有配对的 response有附件时它单独成一条 model;没附件时折进 ack 那条,避免 model→model 相邻(:819-851)
手动 /compress 时尾部 functionCall 是孤儿手动触发直接切掉它(chatCompressionService.ts:692-706)

RESUME_TRAILER(postCompactAttachments.ts:486)那句「照着摘要继续干,别致谢、别重新自我介绍、别再打招呼」不写在压缩提示里,而是这里贴一次。理由:写进提示等于每次压缩都让摘要模型重新生成一遍这句话,浪费输出 token 还会措辞漂移。

stripAnalysisBlock(:524)扫两遍:先剥闭合的 <analysis>…</analysis>(/g 处理多个),再剥未闭合<analysis>…$——模型输出 token 用完、没来得及闭合的情况,不补这一刀整个草稿会漏进历史。

7.4 微压缩:不花模型钱的那一档

microcompactHistory(packages/core/src/services/microcompaction/microcompact.ts:478)是规则式的,三种触发(:409):

触发条件清什么
force/compress-fast 主动调老工具结果 + 媒体
idle距上次 API 完成超过阈值时长老工具结果 + 媒体
size工具结果累计字符数超阈值只清老工具结果

保留策略是每类各留 N 条而不是总共 N 条(microcompact.ts:518-526):toolResultsNumToKeep: 1 意味着工具结果留 1、顶层媒体留 1、嵌套媒体留 1。注释说这更符合用户配置这个值时的直觉。

被清空的内容换成占位符 [Old tool result content cleared] / [Old inline media cleared: …](microcompact.ts:14-15)。有个连带效应必须处理:文件读取有快速路径缓存,如果某个文件的读取结果被清空了,缓存里那条得解除武装,否则会命中一个已经变成占位符的陈旧条目(issue #4239)。所以元数据里带 evictedReadPaths;万一某条路径恢复不出来(provider 没填 functionCall.id),unresolvedEvictedReads 非零,调用方必须退回全量清缓存(microcompact.ts:455-463)。

GeminiChat.compressFast()(packages/core/src/core/geminiChat.ts:2174)把微压缩和「剥掉模型轮次的 thinking 部分」串起来,全程零模型调用。token 数的处理很实在:用同一个估算器算前后差值,然后从API 权威的 lastPromptTokenCount 里减掉这个差值,而不是整个换成估算值(geminiChat.ts:2181-2219)。

GeminiChat.tryCompress()(geminiChat.ts:2088)则是完整压缩的入口——成功后写入新历史、清文件读缓存、更新 token 计数、把连续失败计数归零。

7.5 会话记录与恢复

  • ChatRecordingService(packages/core/src/services/chatRecordingService.ts:524)把每轮写进 <projectDir>/chats/<sessionId>.jsonl,压缩事件也记一条(recordChatCompression,:1143)。这些 JSONL 正是 §5.5 里「做梦」去 grep 的素材。
  • SessionService(packages/core/src/services/sessionService.ts:282)负责列表、删除、恢复;buildApiHistoryFromConversation(:1451)把落盘记录还原成 API 历史,getResumeTokenCounts(:1615 附近)连 token 计数一起还原,这样恢复出来的会话不会以为自己是空的、错过压缩时机。
  • generateSessionRecap(packages/core/src/services/sessionRecap.ts:42)生成一句「你上次干到哪了」。它只取最近 30 条对话消息,filterToDialog(:143)把工具调用、工具结果、以及带 thought/thoughtSignature 的部分全滤掉——前者会用一个 10K token 的文件 dump 淹没 recap 模型,后者会把隐藏推理泄漏成用户可见的摘要。takeRecentDialog(:165)保证切片从一条 user 消息开始,不留悬空的模型回复。

8. 上下文之外的预热:followup 推测执行

前面讲的都是「往窗口里塞什么」。packages/core/src/followup/ 反过来问:能不能在用户还没确认之前就先把活干了?

startSpeculation(packages/core/src/followup/speculation.ts:123)在建议刚展示给用户时就 fork 一个会话,在后台跑起来(上限 MAX_SPECULATION_TURNS = 20 轮、MAX_SPECULATION_MESSAGES = 100 条)。用户按 Tab/Enter 接受 → acceptSpeculation(:420)把结果落到真实文件系统;用户开始打字 → abortSpeculation(:466)清理掉。

安全靠 OverlayFs(packages/core/src/followup/overlayFs.ts:22)这个写时复制层:写请求被重定向到 tmpdir/qwen-speculation/<pid>/<id>/ 下的影子路径(redirectWrite,:57),读请求先查影子、没有再落回真实路径(resolveReadPath,:45)。没被接受的推测,对真实工作区零副作用。

它和本章其余部分共享同一套 fork 机制(runForkedAgent / getCacheSafeParams),也就是说:记忆抽取、做梦、推测执行是同一个「分身」原语的三种用法。多智能体的完整图景见 06 多智能体


9. 巧妙之处(可以带走的)

  1. 把清单挪出 tools 块,换回整段 prompt cache。 技能清单不进工具描述、只进 <system-reminder>,因为 tools 块是缓存前缀的最前面(packages/core/src/tools/skill.ts:41-49)。同一份信息,位置决定成本。
  2. 给模型说人话的时间,而不是时间戳。 memoryAge 输出 "47 days ago" 而不是 ISO 串,因为模型不会做日期算术,触发不了过期推理(packages/core/src/memory/memoryAge.ts:16-20)。
  3. 判空要判「处理后的」结果。 摘要判空判的是剥掉 <analysis> 之后的文本——判原文会得到「指标全绿但 agent 完全失忆」(packages/core/src/services/chatCompressionService.ts:573-581)。
  4. 靠文件路径反推做了什么,而不是要求模型回 JSON。 抽取分身只管写文件,touchedTopicsFromFilePaths 从写过的路径反推 topic 和 scope,少一个模型可能搞砸的结构化输出环节(extractionAgentPlanner.ts:307)。
  5. 失败隔离要分方向。 抽取里 project 层索引重建失败必须抛(游标不前进 → 下次重试),user 层失败只 warn(只读的 ~/.qwen/memories/ 不该毒害 project 层),注释把这个不对称写得很清楚(packages/core/src/memory/extract.ts:172-197)。
  6. 别让「加固」把功能改坏。 索引里的危险文件名一度被改写成 _,注入是防住了,但链接指向了不存在的文件;换成百分号编码后两个目标同时成立(packages/core/src/memory/indexer.ts:75-106)。symlink 的越界检查被整个撤掉也是同一类判断(packages/core/src/skills/symlinkScope.ts:18-31)。
  7. 预取 + 零等待轮询。 记忆召回一进门就发,组装请求时只做非阻塞轮询,没 settle 就等下一个 ToolResult 轮次——用户永远不为召回等待(packages/core/src/core/client.ts:2009:2129)。
  8. 锁要记 PID 并探活。 做梦的锁文件写入持有者 PID,检查时 process.kill(pid, 0) 探活,加上一小时的陈旧上限和释放失败的自愈标志(packages/core/src/memory/manager.ts:365-402:1024-1035)。

10. 边界与局限

  • 抽取和做梦要花模型钱。 各自是一次完整的 fork 子代理运行(5 轮 / 8 轮预算)。这是用后台 token 换未来上下文效率的一笔明确交易。
  • 召回选择器只看 header。 模型选择器拿不到正文(relevanceSelector.ts:54 的 manifest 只有 type / path / 时间 / description)。description 写糊,这条记忆基本就召不回来了。
  • 索引硬上限 200 行 / 25KB。 超了就截断并塞一条 WARNING(indexer.ts:119-136prompt.ts:137)。记忆规模只能靠做梦压着,没有分片索引。
  • 摘要截断只能靠猜。 >= COMPACT_MAX_OUTPUT_TOKENS 是个启发式,会对恰好卡在上限的正常摘要误判。代码里挂了 TODO:真正的信号是 finish_reason === 'length',但 runSideQuery 目前不透出(chatCompressionService.ts:937-941)。
  • 技能监听深度写死为 2。 深层嵌套的技能布局不会被察觉。这是为了不耗尽 fd 的明确取舍(skill-manager.ts:56-59)。
  • @import 深度上限 5。 超了停止处理并 warn,不报错也不失败(memoryImportProcessor.ts:228)。
  • team 记忆的同步是 git,不是协议。 依赖仓库有 upstream、目录没被 .gitignore 吞掉。两种情况下代码都只发 debug/warn,功能静默不生效(config.ts:2383-2403:2433-2441)。
  • 密钥扫描只覆盖高置信度前缀。 有意选的 gitleaks 子集,不是全面 DLP——自定义格式的凭证照样能写进 team 记忆。

11. 代码地图(导航索引)

主题文件路径关键符号
系统提示装配packages/core/src/core/prompts.tsgetCoreSystemPromptgetCustomSystemPromptbuildSystemPromptSuffixgetActionsSectiongetToolCallExamplesresolvePathFromEnv
压缩 / 摘要提示packages/core/src/core/prompts.tsgetCompressionPromptgetProjectSummaryPrompt
主会话系统指令packages/core/src/core/client.tsgetMainSessionSystemInstruction
上下文文件命名packages/core/src/memory/const.tsDEFAULT_CONTEXT_FILENAMEAGENT_CONTEXT_FILENAMELOCAL_CONTEXT_FILENAMEgetAllGeminiMdFilenames
命名常量的轻量再导出packages/core/src/tools/memory-config.ts(全文即 re-export)
分层发现packages/core/src/utils/memoryDiscovery.tsloadServerHierarchicalMemorygetGeminiMdFilePathsInternalForEachDirconcatenateInstructionscreateMemoryTypeClassifier
@import 处理packages/core/src/utils/memoryImportProcessor.tsprocessImportsfindImportsfindCodeRegionsvalidateImportPath
记忆提示段packages/core/src/memory/prompt.tsbuildManagedAutoMemoryPromptappendManagedAutoMemoryToUserMemoryTYPES_SECTION_INDIVIDUALWHAT_NOT_TO_SAVE_SECTION
记忆路径与三层作用域packages/core/src/memory/paths.tsgetAutoMemoryRootgetUserAutoMemoryRootgetTeamAutoMemoryRootisAnyAutoMemPathisTeamAutoMemPath
抽取(游标)packages/core/src/memory/extract.tsrunAutoMemoryExtract
抽取(分身与权限)packages/core/src/memory/extractionAgentPlanner.tsrunAutoMemoryExtractionByAgentcreateMemoryScopedAgentConfigtouchedTopicsFromFilePaths
抽取(已废弃的旧规划器)packages/core/src/memory/extractionPlanner.ts(空壳,仅注释)
做梦packages/core/src/memory/dream.tsdreamAgentPlanner.tsrunManagedAutoMemoryDreamplanManagedAutoMemoryDreamByAgentbuildConsolidationTaskPrompt
召回packages/core/src/memory/recall.tsresolveRelevantAutoMemoryPromptForQueryselectRelevantAutoMemoryDocumentsbuildRelevantAutoMemoryPrompt
召回(模型选择器)packages/core/src/memory/relevanceSelector.tsselectRelevantAutoMemoryDocumentsByModelformatMemoryManifest
新鲜度packages/core/src/memory/memoryAge.tsmemoryAgememoryFreshnessTextmemoryFreshnessNote
索引packages/core/src/memory/indexer.tsrebuildManagedAutoMemoryIndexrebuildTeamAutoMemoryIndexsanitizeIndexFieldencodeIndexPathTargetTeamMemoryRootSecurityError
脚手架与读取packages/core/src/memory/store.tsensureAutoMemoryScaffoldreadAutoMemoryIndexensureUserAutoMemoryScaffold
条目解析packages/core/src/memory/entries.tsparseAutoMemoryEntriesrenderAutoMemoryBodymergeAutoMemoryEntry
密钥拦截packages/core/src/memory/secret-scanner.tsteam-memory-secret-guard.tsscanForSecretscheckTeamMemorySecrets
遗忘packages/core/src/memory/forget.tsselectManagedAutoMemoryForgetCandidatesforgetManagedAutoMemoryMatchesforgetManagedAutoMemoryEntries
记忆统一入口packages/core/src/memory/manager.tsMemoryManagerscheduleExtractscheduleDreamrecallforgetgetStatusdraindreamLockExists
记忆的触发点packages/core/src/core/client.tsrunManagedAutoMemoryBackgroundTaskstryConsumeMemoryPrefetchcancelPendingMemoryPrefetchMemoryPrefetchHandle
技能管理packages/core/src/skills/skill-manager.tsSkillManagerWATCHER_MAX_DEPTHrefreshCachestartWatchingparseSkillContentwatcherIgnored
技能条件激活packages/core/src/skills/skill-activation.tsSkillActivationRegistrysplitConditionalSkillsmatchAndConsume
技能加载与 symlinkpackages/core/src/skills/skill-load.tssymlinkScope.tsloadSkillsFromDirvalidateSymlinkTarget
技能工具packages/core/src/tools/skill.tsskill-utils.tsSkillToolSKILL_TOOL_DESCRIPTIONbuildSkillLlmContent
压缩服务packages/core/src/services/chatCompressionService.tsChatCompressionService.compresscomputeThresholdsCOMPACT_MAX_OUTPUT_TOKENSMAX_CONSECUTIVE_FAILURES
压缩输入瘦身packages/core/src/services/compactionInputSlimming.tsslimCompactionInputresolveCompactionTuningestimateContentChars
压缩后附件packages/core/src/services/postCompactAttachments.tscomposePostCompactHistorypostProcessSummarystripAnalysisBlockRESUME_TRAILER
微压缩packages/core/src/services/microcompaction/microcompact.tsmicrocompactHistoryevaluateTimeBasedTriggerMicrocompactMeta
压缩入口packages/core/src/core/geminiChat.tstryCompresscompressFast
压缩状态枚举packages/core/src/core/turn.tsCompressionStatusCompactionTriggerReason
会话记录packages/core/src/services/chatRecordingService.tsChatRecordingServicerecordChatCompression
会话恢复packages/core/src/services/sessionService.tsSessionServicebuildApiHistoryFromConversationgetResumeTokenCounts
会话回顾packages/core/src/services/sessionRecap.tsgenerateSessionRecapfilterToDialogtakeRecentDialog
推测执行packages/core/src/followup/speculation.tsoverlayFs.tsstartSpeculationacceptSpeculationabortSpeculationOverlayFs
分身与侧查询原语packages/core/src/utils/forkedAgent.tssideQuery.tsrunForkedAgentgetCacheSafeParamsrunSideQuery