Qwen Code Daemon 无会话工作区记忆 APISessionless Managed Memory 任务队列的设计与实现【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读在 Qwen Codeqwen-code的 daemon 架构中托管记忆managed memory的自动抽取、dream 压缩等能力此前必须依托一个活跃的聊天会话才能写入。本文基于 daemon-workspace-remember 设计文档结合仓库中的 workspace-remember.ts、remember.ts 等源码实现完整讲解这套无会话sessionless工作区记忆任务 API它如何在「不创建、不污染任何可见会话」的前提下完成 remember / forget / dream 三类记忆操作如何通过串行任务车道task lane保证并发安全以及如何通过 202 轮询的异步契约让设置面板、SDK 调用方获得可靠的状态反馈。读完本文你将掌握这套 API 的完整端点契约、任务生命周期、容量与超时策略、错误码体系以及从 HTTP 路由到 ACP 子进程、再到核心记忆写入逻辑的完整调用链。1. 问题背景为什么记忆写入必须摆脱会话依赖在引入本设计之前daemon 的托管记忆系统自动抽取、dream agent要求必须存在一个活跃的聊天会话才能写入记忆这带来了两个现实问题设置面板无法写入记忆——web-shell 的设置界面需要保存用户提供的事实例如「所有编辑器一律使用 TypeScript strict mode」但为此创建或污染一个可见的聊天会话显然不合理。会话列表被污染——为了执行一次/remember命令而临时创建一个「用完即弃」的会话会在会话列表里留下大量用户从未主动打开过的「幽灵会话」造成困惑。解决思路是一个无会话的工作区级记忆任务 API它负责排队 remember、forget、dream 三类任务在不创建可见会话的前提下执行它们并通过轮询暴露执行状态。需要说明的是本文描述的设计方案在仓库中标记为ProposedPR #5884分支codex/sessionless-daemon-remember但对应实现文件已经存在于当前仓库源码中见下文各小节引用的具体路径本文以源码现状为准展开讲解。2. 设计概览一条从客户端到 ACP 子进程的完整链路整体架构如下摘自设计文档┌──────────────┐ POST /workspace/memory/{task} ┌─────────────────────────┐ │ SDK / UI │ ─────────────────────────────────► │ workspace-remember.ts │ │ client │ │ (WorkspaceRemember- │ │ │ GET /workspace/memory/{task}/:id │ TaskLane) │ │ │ ─────────────────────────────────► │ │ └──────────────┘ └────────────┬────────────┘ │ bridge.runWorkspaceMemory* ┌────────────▼────────────┐ │ HttpAcpBridge │ │ extMethod( │ │ qwen/control/ │ │ workspace/memory/ │ │ {task}) │ └────────────┬────────────┘ │ ACP stdio (JSON-RPC) ┌────────────▼────────────┐ │ qwen --acp child │ │ (QwenAgent.extMethod) │ │ → remember / forget / │ │ dream core logic │ └─────────────────────────┘这套设计具备四个关键性质无需会话No session required——bridge 只确保 ACP 子进程被拉起但绝不创建 / 加载 / 恢复任何 ACP 会话。这一点在 bridgeTypes.ts 的接口注释中明确声明「必须确保 ACP child 存在但不得创建/加载/恢复 ACP 会话也不得触碰按会话的 prompt 队列」。串行执行Serial execution——任务通过一条 promise-chain 车道逐条执行防止对托管记忆文件系统产生并发写入。隐藏Hidden——remember/dream 通过隐藏 agent 运行forget 使用隐藏记忆配置所有操作都不会产生可见会话。能力广播Capability-advertised——daemon 的/capabilities响应中会声明workspace_memory_remember、workspace_memory_forget、workspace_memory_dream三项能力remember 还会附带modes: [workspace, clean]。从 SDK 源码看还衍生出workspace_memory_remember_project_scope、workspace_memory_remember_user_scope、workspace_memory_forget_scope等更细粒度的能力标签用于旧 daemon 兼容性预检见 DaemonClient.ts。3. HTTP API 端点详解所有端点都挂在/workspace/memory/{task}路径下采用「POST 入队 GET 轮询」的异步任务模式。POST 统一返回202 Accepted与任务快照客户端随后轮询GET /workspace/memory/{task}/:taskId直到任务进入终态。3.1POST /workspace/memory/remember—— 入队一个 remember 任务请求体{ content: The user prefers dark mode in all editors, contextMode: workspace }FieldTypeRequiredDescriptioncontentstringyes要记住的事实。最大64 KiBUTF-8 字节长度。contextModestringnoworkspace默认——agent 可看到工作区记忆上下文clean——agent 看不到任何先前的用户记忆。scopestringno目标范围project或user省略时由 agent 自动选择源码中新增的可选字段见 workspace-remember.ts。HeadersAuthorization: Bearer token必填X-Qwen-Client-Id: clientId可选——用于限定任务的可见范围响应202 Accepted{ taskId: remember-a1b2c3d4-..., status: queued, contextMode: workspace, createdAt: 2026-06-01T12:00:00.000Z, updatedAt: 2026-06-01T12:00:00.000Z }错误响应StatusCodeCondition400invalid_contentcontent 缺失、为空或超出 64 KiB400invalid_context_modecontextMode 不是workspace或clean400invalid_scopescope 不是project或user源码新增400invalid_client_idX-Qwen-Client-Id 未在 bridge 注册403untrusted_workspace工作区未被信任源码新增防护409managed_memory_unavailable该工作区未配置托管记忆429remember_queue_full已有 16 个待处理任务排队500remember_failed可用性检查意外抛错实现细节路由层在入队前会做多道校验——content经trim()后必须非空且Buffer.byteLength(trimmedContent, utf8)不得超过常量MAX_REMEMBER_CONTENT_BYTES 64 * 1024定义于 workspace-memory-remember-constants.tsX-Qwen-Client-Id必须存在于bridge.knownClientIds()集合中workspace-remember.ts随后调用isWorkspaceMemoryRememberAvailable()做可用性预检以便在入队前同步返回409workspace-remember.ts。3.2GET /workspace/memory/remember/:taskId—— 轮询 remember 任务Headers与 POST 相同X-Qwen-Client-Id必须与发起方一致才能看到任务。响应200 OKqueued / running{ taskId: remember-a1b2c3d4-..., status: queued, contextMode: workspace, createdAt: 2026-06-01T12:00:00.000Z, updatedAt: 2026-06-01T12:00:00.000Z, result: null, error: null }status为queued或running取决于任务是否已开始执行result仅在status completed时非空error仅在status failed时非空。响应200 OKcompleted{ taskId: remember-a1b2c3d4-..., status: completed, contextMode: workspace, createdAt: 2026-06-01T12:00:00.000Z, updatedAt: 2026-06-01T12:00:05.000Z, result: { summary: Saved dark-mode preference to user memory., filesTouched: [~/.qwen/memories/user/user.md], touchedScopes: [user] } }响应200 OKfailed{ taskId: remember-a1b2c3d4-..., status: failed, contextMode: workspace, createdAt: 2026-06-01T12:00:00.000Z, updatedAt: 2026-06-01T12:00:03.000Z, error: { code: remember_path_escape, message: Remember agent touched a path outside managed memory. } }错误响应StatusCodeCondition400invalid_client_idX-Qwen-Client-Id 未注册404remember_task_not_found任务不存在或属于其他 client可见性语义源码印证任务快照通过lane.get(taskId, requesterClientId, kind)获取workspace-remember.ts——若任务携带originatorClientId则请求方必须与之完全一致若任务未携带而请求方带了 clientId同样返回不可见。这保证了任务的多客户端隔离。3.3POST /workspace/memory/forget—— 入队一个 forget 任务forget 任务由 daemon 选出匹配的托管自动记忆条目并删除全程不创建会话。请求体{ query: old preference }FieldTypeRequiredDescriptionquerystringyes用自然语言描述要遗忘的内容。最大64 KiBUTF-8 字节长度。scopestringno限定遗忘范围project/user省略则两边都查源码新增。初始响应为202 Accepted返回forget-...前缀的 taskId随后轮询GET /workspace/memory/forget/:taskId直到终态。completed 结果{ summary: Forgot 1 memory entry., removedEntries: [ { topic: project, summary: old preference, filePath: /path/to/memory.md } ], touchedTopics: [project], touchedScopes: [project] }3.4GET /workspace/memory/forget/:taskId轮询 forget 任务。结构上与 remember 轮询一致差异在于没有contextMode字段未知或未授权的 taskId 返回forget_task_not_found。3.5POST /workspace/memory/dream—— 入队一个 dream 任务dream 任务执行托管自动记忆的 dream 压缩流程跨条目去重、汇总同样不创建会话。请求体空 JSON 对象或省略 body。初始响应为202 Accepted返回dream-...前缀的 taskId轮询GET /workspace/memory/dream/:taskId直到终态。completed 结果{ summary: Managed auto-memory dream completed., touchedTopics: [project], dedupedEntries: 1 }3.6GET /workspace/memory/dream/:taskId轮询 dream 任务结构与 remember 轮询一致无contextMode字段未知或未授权的 taskId 返回dream_task_not_found。4. 任务生命周期与容量控制4.1 状态机enqueue() │ ▼ ┌─────────────────────┐ │ queued │ (awaiting serial lane slot) └──────────┬──────────┘ │ lane picks up ▼ ┌─────────────────────┐ │ running │ (bridge.runWorkspaceMemoryRemember in progress) └──────────┬──────────┘ │ ┌───────┴────────┐ ▼ ▼ ┌──────────┐ ┌──────────┐ │ completed│ │ failed │ └──────────┘ └──────────┘queued——任务已创建正在串行车道中等待。running——bridge 调用在途forked agent 正在执行。completed——agent 成功结束result已填充。failed——agent 抛错或超时error已填充。4.2 容量与保留策略车道级常量定义在 workspace-remember.ts 的WorkspaceRememberTaskLane中常量值含义MAX_TASKS1000车道最多保存1000 条任务记录达到上限后按 FIFO 逐出终态任务evictTerminalTasksTERMINAL_TASK_TTL_MS5 分钟终态任务completed/failed超过该 TTL 后也会被清理MAX_PENDING16同一时刻最多16 条待处理任务queued runningMAX_NON_REMEMBER_PENDING8forget/dream 两类任务合计最多8 条待处理两级容量设计assertCapacityworkspace-remember.ts的意图很明确forget 和 dream 这类「突发性手工维护」共享较小的子配额避免突发维护耗尽 remember 自动写入所需的全部车道槽位——例如自动记忆抽取是常态工作负载不能被一次批量 dream 挤掉。另外beginDrain()/dispose()提供了工作区下线语义dispose()会把所有 queued 任务标记为failedworkspace_removed错误正在 running 的任务在结束后也会被failRunningTaskAfterRemoval修正为失败workspace-remember.ts。5. 源码级实现剖析5.1 串行任务车道WorkspaceRememberTaskLane车道位于 workspace-remember.ts核心是一个MaptaskId, TaskRecord加一条单一 promise 链this.tail。每次enqueue()/enqueueForget()/enqueueDream()都会向链尾追加一个run函数其执行流程为将任务状态置为running调用对应的 bridge 方法runWorkspaceMemoryRemember/runWorkspaceMemoryForget/runWorkspaceMemoryDream成功置为completed填充result若任务确实改写了托管记忆则发布memory_changed事件失败置为failed用稳定的公开错误码填充error。三条enqueue*路径还各有额外校验remember 在result.filesTouched.length 0时抛remember_no_update「agent 没有更新任何记忆」发布事件前会通过publishManagedMemoryChanged检查touchedScopes非空workspace-remember.ts。车道的本质是严格串行化——任意时刻只有一个工作区记忆任务在执行从根本上杜绝了对托管记忆文件系统的并发写入详见第 8 节「为什么串行」。5.2 Bridge 层HttpAcpBridge工作区记忆方法在BridgeInterfacebridgeTypes.ts中新增了四个方法isWorkspaceMemoryRememberAvailable()——调用子进程的qwen/control/workspace/memory/remember/availabilityext-method返回boolean。用于在入队前快速失败409。runWorkspaceMemoryRemember(request)——调用qwen/control/workspace/memory/rememberext-method超时300 sWORKSPACE_MEMORY_REMEMBER_TIMEOUT_MS。不创建也不加载会话。runWorkspaceMemoryForget(request)——调用qwen/control/workspace/memory/forgetext-method使用同一 bridge 超时。不创建也不加载会话。runWorkspaceMemoryDream()——调用qwen/control/workspace/memory/dreamext-method使用同一 bridge 超时。不创建也不加载会话。这些方法都会调用ensureChannel()必要时拉起 ACP 子进程并在没有活跃会话时重启空闲计时器。5.3 ACP 子进程执行QwenAgent.extMethod在 acpAgent.ts 中workspaceMemoryRemember/workspaceMemoryForget/workspaceMemoryDream的 handler 执行以下步骤校验任务级入参remember 的content/contextMode/scopeforget 的query检查config.isManagedMemoryAvailable()调用对应的核心操作并携带一个295 s的 abort 信号WORKSPACE_MEMORY_REMEMBER_CHILD_TIMEOUT_MS 295_000见 acpAgent.ts。forget 场景下该信号会贯穿MemoryManager.forget的条目选择、模型侧查询和落盘文件系统变更全过程。值得一提的细节子进程侧使用AbortSignal.timeout()构造超时超时后错误被映射为remember_timeout等带稳定 code 的错误acpAgent.ts而不是传输层超时——这正是第 6 节超时链设计的落地。5.4 核心 remember 逻辑packages/core/src/memory/remember.tsrunManagedRememberByAgent()remember.ts是 remember 的最终执行者流程如下通过buildCleanMemorySystemPrompt()从项目的托管记忆索引构建一份干净的记忆系统提示词若contextMode clean通过getUserMemory: () 剥离先前的用户记忆实现「白板」写入同时createHiddenRememberConfig()会禁用聊天记录、转录路径clean 模式下还会禁用全部 hooksremember.ts创建memoryScopedAgentConfig见 5.5把文件 I/O 严格限制在记忆目录内用runForkedAgent运行一个forked 无头 agent名称managed-auto-memory-remember工具read_file、grep、ls、write_file、edit最大轮数6getMemoryAgentMaxTurns() ?? 6最大时长5 分钟getMemoryAgentTimeoutMinutes() ?? 5用classifyTouchedScopes()校验所有被写入的文件都位于允许的记忆路径内若 agent 写到了记忆目录之外抛出remember_path_escaperemember.ts对任何被触碰的 scope 重建记忆索引rebuildManagedAutoMemoryIndex/rebuildUserAutoMemoryIndex——这是关键一致性保障MEMORY.md会逐字加载进每个未来的会话因此失败、取消的 run 也必须在报错前重建索引绝不允许把手工写入的索引文件留在磁盘上见 remember.ts 中覆盖 failed/cancelled/escape/no-update 各路径的重建逻辑返回{ summary, filesTouched, touchedScopes }。此外还有两道额外防线remember_no_updateagent 完成但未写入任何条目和remember_scope_mismatch指定了scope但 agent 写到了范围之外。5.5 记忆作用域 agent 配置memory-scoped-agent-config.tscreateMemoryScopedAgentConfig()memory-scoped-agent-config.ts创建一份权限受限的Config包装器把原PermissionManager替换为MemoryScopedPermissionManager写工具write_file、edit仅允许写入项目自动记忆根目录或用户记忆根目录~/.qwen/memories读工具read_file、grep、ls当restrictReadsToMemoryPaths为 true 时仅允许读取记忆目录内Shell默认禁用若allowShell开启仅允许只读命令通过isShellCommandReadOnlyASTInDirectory做 AST 级只读判定memory-scoped-agent-config.tsSymlink 解析所有路径判定前先realpath解析防止通过符号链接逃逸出记忆目录realpathExistingOrNew/realpathNewPathmemory-scoped-agent-config.tsPinned 记忆保护protectPinnedMemory开启时pinned/目录被声明为只读任何对其的写入直接denymemory-scoped-agent-config.ts决策合并作用域决策与基础决策按优先级deny ask allow default合并bypassBaseAskForScopedPaths开启时允许用allow覆盖基础配置的askmemory-scoped-agent-config.ts。该模块配有独立的单元测试 memory-scoped-agent-config.test.ts覆盖路径逃逸、pinned 保护、只读 shell 等边界场景。6. 事件通知memory_changedscope:managed当某个工作区记忆任务成功完成且确实触碰了托管记忆时daemon 会在 SSE 事件流GET /session/:id/events上发布一个memory_changed事件scope为managed。订阅了按会话事件流的客户端都会收到该通知。Payload{ type: memory_changed, data: { scope: managed, source: workspace_memory_remember, taskId: remember-a1b2c3d4-..., touchedScopes: [user, project] } }FieldTypeDescriptionscopemanaged与基于文件的memory_changed事件scope: workspace | global区分开sourcestringworkspace_memory_remember、workspace_memory_forget或workspace_memory_dreamtaskIdstring与 POST 返回的任务 ID 关联touchedScopesstring[]变更了哪些托管记忆 scopeuser、projectPOST 时若提供了originatorClientId它会被附加到事件信封上使事件总线能将通知路由回发起方客户端workspace-remember.ts。事件只在touchedScopes非空时发布。7. 错误处理与超时链7.1 错误码全景CodeOriginMeaninginvalid_contentHTTP routecontent 缺失、为空或超过 64 KiBinvalid_context_modeHTTP routecontextMode 不是workspace或cleaninvalid_queryHTTP routeforget query 缺失、为空或超过 64 KiBinvalid_scopeHTTP routescope 不是project或user源码新增invalid_client_idHTTP routeClient-Id header 不在 bridge 的已知集合中managed_memory_unavailableBridge / ACP child工作区未配置托管记忆remember_queue_fullTask lane达到 16 条待处理任务上限remember_path_escapeCore remember logicAgent 写到了托管记忆目录之外的路径remember_scope_mismatchCore remember logicAgent 写到了请求 scope 之外源码新增remember_no_updateCore remember logicAgent 未更新任何记忆源码新增remember_failedCatch-all未分类的 agent 失败、超时或内部错误remember_task_not_foundHTTP routeGET 未知或未授权的任务 IDforget_task_not_foundHTTP routeGET 未知或未授权的 forget 任务 IDdream_task_not_foundHTTP routeGET 未知或未授权的 dream 任务 IDworkspace_removedTask lane工作区运行时被移除源码新增dispose路径workspace_drainingTask lane工作区运行时正在被移除源码新增入队时拒绝remember_timeout/forget_timeout/dream_timeoutACP child子进程侧超时源码新增这些错误码的公开消息与 HTTP 状态映射集中在 workspace-remember.tspublicErrorMessage/publicErrorStatus以及 workspace-remember-errors.tsworkspaceMemoryFailureCode/workspaceMemoryFailureDiagnostics中并配有单元测试 workspace-remember-errors.test.ts。注意部分错误如remember_path_escape通过shouldSuppressRememberErrorDetails抑制内部细节外泄避免把敏感路径暴露给客户端。7.2 超时链Agent forked runner: 5 min maxTimeMinutes Child abort signal: 295 s (WORKSPACE_MEMORY_REMEMBER_CHILD_TIMEOUT_MS) Bridge timeout: 300 s (WORKSPACE_MEMORY_REMEMBER_TIMEOUT_MS)子进程在 bridge 超时之前先 abort从而保证错误以干净的语义错误remember_timeout等向上传播而不是传输层超时。这条「子进程先于 bridge 兜底」的链式设计配合任务车道层 5 分钟终态 TTL构成了完整的超时与资源回收机制。8. SDK 集成8.1 TypeScript SDKqwen-code/sdk-typescriptDaemonClient上暴露了对应方法DaemonClient.ts// Queue a remember task const task await client.rememberWorkspaceMemory( The project uses pnpm workspaces, { contextMode: workspace }, ); // task.taskId, task.status queued // Poll until terminal const result await client.getWorkspaceMemoryRememberTask(task.taskId); // result.status completed | failed const forget await client.forgetWorkspaceMemory(old preference); const forgetResult await client.getWorkspaceMemoryForgetTask(forget.taskId); const dream await client.dreamWorkspaceMemory(); const dreamResult await client.getWorkspaceMemoryDreamTask(dream.taskId);SDK 侧还做了能力预检当调用方传入scope时SDK 会先通过requireCapability()检查对应能力标签workspace_memory_remember_project_scope/workspace_memory_remember_user_scope/workspace_memory_forget_scope因为旧版 daemon 会静默忽略scope参数——预检把「静默降级」变成「响亮的错误」避免无 scope 的 forget 误删两个存储中的条目DaemonClient.ts。这些方法在 DaemonClient.test.ts 中有完整的单测覆盖。8.2 UI 事件规范化SDK 的 normalizer 把原始 SSE 的memory_changedscope: managed事件映射为DaemonUiWorkspaceMemoryChangedEventnormalizer.ts、types.ts{ type: workspace.memory.changed, scope: managed, source: workspace_memory_remember, taskId: remember-..., touchedScopes: [user, project] }这是对既有workspace.memory.changed事件类型的扩展——该事件此前只承载scope: workspace | global基于文件的 QWEN.md 写入如今新增managed分支。事件在 UI 层的terminal.ts、transcript.ts中按新分支分发并有 daemonUi.test.ts 验证。9. 设计权衡为什么这样设计为什么无会话Why sessionlessCLI 中的/remember斜杠命令本身已经能在会话内工作但设置面板和程序化 SDK 调用方不应该为了持久化一条事实而创建一个会话。会话意味着对话历史、回合跟踪和在会话列表中的可见性——这些对一个「即发即忘」的记忆写入来说全部是多余的负担。无会话 API 让记忆写入成为一等公民操作与聊天会话完全解耦。为什么串行执行Why serial execution托管记忆系统把事实存储在带索引的 markdown 文件中。多个 remember 任务并发写入可能损坏索引或产生合并冲突——单线程车道是最简单且正确的解法结合 5.1 节可知这是由一条 promise 链this.tail保证的硬性串行约束。为什么用任务队列而非同步执行Why a task queue记忆写入涉及 LLM agent 决定「在哪里、如何存」这条事实在 user 与 project scope 之间选择、挑选正确的文件、格式化通常耗时2–30 秒。同步 HTTP 请求要么超时、要么阻塞客户端。异步「队列 轮询」模式保持了 HTTP 契约的简洁也让客户端能展示进度 UI。为什么需要contextModeworkspace默认——remember agent 能看到既有记忆作为上下文从而对已有条目去重或更新clean——agent 看不到任何先前的用户记忆适合调用方希望强制全新写入、跳过去重逻辑的场景例如批量导入。为什么限制读取路径Why restrict reads to memory pathsremember agent 只应读写托管记忆目录内的内容。这能阻止一类提示注入攻击精心构造的content诱导 agent 去读取敏感的项目文件并把内容泄漏进记忆条目。restrictReadsToMemoryPaths配合 symlink 解析5.5 节从权限层面对此做了硬性封堵。10. 延伸阅读设计文档原文daemon-workspace-remember.md车道与路由实现workspace-remember.ts 及其测试 workspace-remember.test.ts错误码与诊断workspace-remember-errors.ts核心 remember 逻辑remember.ts 及其测试 remember.test.ts记忆作用域权限包装器memory-scoped-agent-config.tsBridge 接口定义bridgeTypes.tsACP 子进程 ext-method 处理acpAgent.tsSDK 客户端方法与 UI 事件规范化DaemonClient.ts、normalizer.ts相关设计背景托管记忆体系managed-memory-microcompaction、auto-memory 设计目录【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考