qwen-code Daemon 会话恢复:ask_user_question 挂起问题跨重启还原(--restore-ask-user-question)设计解析

qwen-code Daemon 会话恢复:ask_user_question 挂起问题跨重启还原(--restore-ask-user-question)设计解析 qwen-code Daemon 会话恢复ask_user_question 挂起问题跨重启还原--restore-ask-user-question设计解析【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-codeask_user_question是 qwen-code 中模型在执行过程中向用户提问的 HITLHuman-in-the-Loop机制其提问状态保存在 daemon 进程内存中。一旦 daemon 重启session/load与session/resume会把会话末尾未回答的问题当作失败的工具调用关闭orphan repair finalizeDangling导致用户与模型之间的实时问答被静默丢弃。本设计文档围绕--restore-ask-user-question开关展开讲述如何在加载/恢复会话时把尾部未回答的ask_user_question用新的 requestId重新挂起re-hang让原有权限投票路由继续提交答案、真实函数响应回到模型、同一轮对话无缝续跑。读完本文你将掌握该功能的资格判定规则、开关配置方式、五步运行时调用链以及它与continueLastTurn、orphan repair、transcript replay 等既有机制的边界。问题背景内存态 HITL 与重启后的孤儿问题在 daemon 架构下ask_user_question的整个提问—等待—回答生命周期都是进程内存态的daemon 通过 bridge 向客户端发起权限请求等待用户通过POST /session/:id/permission/:requestId提交投票。其核心矛盾在于旧requestId在进程重启后不复存在因此重启后无法再用原来的requestId完成那一次权限请求session/load与session/resume在重建会话时会执行orphan repair孤儿工具调用修复把尾部悬挂的functionCall合成一条失败的工具结果transcript replay 的finalizeDangling会把该轮标记为已终结UI 上表现为提问被静默关闭。结果是用户还没回答的问题在 daemon 重启后被当作一次工具崩溃处理HITL 交互彻底丢失。requestId不会被持久化这一设计决策贯穿整个方案——恢复永远以新 requestId、新超时时钟开始而不是尝试找回旧的权限状态。方案目标与核心行为当--restore-ask-user-question开启默认关闭且客户端加载或恢复该会话时系统执行以下四项行为不合成失败工具结果不再为该尾部ask_user_question伪造一条错误函数响应以新 requestId 重新发起requestPermission超时时钟从零开始旧的内存态 requestId 被彻底抛弃状态对外恢复可见GET /session/:id/status重新返回isWaitingForUserQuestion: true以及pendingInteractions客户端 UI 可据此重新渲染提问卡片投票路由复用用户通过既有的POST /session/:id/permission/:requestId权限投票路由提交答案bridge 把真实函数响应写回模型同一轮对话继续执行而不是开启新的一轮。需要特别强调的是daemon 启动时不会扫描或自动恢复等待中的会话。恢复是加载/恢复动作的副作用只在客户端主动 load/resume 且满足全部资格条件时发生。资格判定什么情况下可以恢复恢复不是无条件的。设计文档列出了一组严格的门槛条件且这些条件与源码中的纯历史扫描逻辑一一对应见 ask-user-question-restore.ts条件说明配置开关为 true--restore-ask-user-question必须显式开启历史最后一条是model轮只有模型最后发出、尚未收到响应的工具调用才可能悬挂该轮所有带 id 的functionCall都是ask_user_question遍历last.parts只要发现任一非 AUQ 的functionCall如run_shell_command立即返回不可恢复参数解析为有效AskUserQuestionParams结构校验非法参数 fail-closed 降级为失败工具结果无混合悬挂工具bash question这类混合批次整体留在 orphan repair 路径live 会话无进行中的 prompt已在等待时不得二次恢复避免重复挂起load/resume 请求携带附加 client id没有 client id 就无人能回答重新挂起的问题keepalive、boot rehydrate、子会话恢复均不带会话不是本次调用中branchSession创建的 forkfork 恢复不挂起主会话限定只有主会话main session可以运行ask_user_questionfork 与 subagent 本身就无法使用该工具因此恢复同样只在主会话路径生效。资格判定如何映射到源码资格扫描是一个纯历史扫描pure history scan不构造工具实例入口函数为findRestorableAskUserQuestion与restorableAskUserQuestionCallIdsask-user-question-restore.tsexport function findRestorableAskUserQuestion( last: Content | undefined, ): RestorableAskUserQuestion | undefined { if (last?.role ! model) return undefined; const functionCalls: FunctionCall[] []; for (const part of last.parts ?? []) { const fc part.functionCall; if (!fc?.id) continue; if (fc.name ! ToolNames.ASK_USER_QUESTION) return undefined; if (!parseAskUserQuestionParams(fc.args)) return undefined; functionCalls.push(fc); } if (functionCalls.length 0) return undefined; return { functionCalls }; }该函数只取历史最后一条记录如chat.peekLastHistoryEntry()避免对整个历史做structuredClone——后者还会丢失normalizeModelToolCallIds附加在 Symbol key 上的 provider 工具调用 id。结构校验函数parseAskUserQuestionParams与AskUserQuestionTool.validateToolParams保持一致questions数量必须在 1–4 之间、每题的question与header必须是非空字符串、options数量在 2–4 之间、每个 option 的label与description必须非空、multiSelect若存在必须是布尔值askUserQuestion.ts。测试覆盖了这些边界ask-user-question-restore.test.ts合法负载可恢复返回 call id 集合混合悬挂工具run_shell_commandask_user_question同一轮不可恢复无悬挂 model 轮纯文本或已有 functionResponse不可恢复非法参数 fail-closed例如只提供一个 option 的题目findRestorableAskUserQuestion返回undefined降级到失败工具结果路径测试注释明确写了这一设计意图。开关配置serve 与 ACP child 双入口开关在 CLI 顶层选项与 serve 子命令中均有定义默认值全部为falseserve 侧serve.tsqwen serve --restore-ask-user-questionACP child 侧须与--acp组合qwen --acp --restore-ask-user-question选项本体定义在 top-level-options.tstype: booleandefault: false描述为在 daemon 会话 load/resume 时重新挂起尾部未回答的 ask_user_question而不是合成失败工具结果。重要约束child 侧标志仅在 ACP 模式下生效CLI 配置测试明确锁定了这一行为config.test.tsqwen --acp --restore-ask-user-question→getRestoreAskUserQuestion()为true仅qwen --restore-ask-user-question无--acp→ 仍为false被忽略qwen --acp不带标志 → 默认false。设计文档解释了原因在纯 TUI 模式下没有任何通道能重新挂起问题若跳过 load 时的 orphan repair恢复后的会话会被永久卡死wedge。因此 child 侧标志只在 ACP 模式下有意义。v1 版本不提供 settings.json key也不提供 capability tag只有命令行开关这一种配置途径。从 serve 到 ACP child 的标志透传当 serve daemon 需要 spawn 一个qwen --acp子进程bridge 默认 spawn 或注入的 channel factory时标志通过 acp-child-extra-args.ts 透传export function acpChildExtraArgs(opts: { experimentalLsp?: boolean; restoreAskUserQuestion?: boolean; }): string[] | undefined { const extraArgs [ ...(opts.experimentalLsp true ? [--experimental-lsp] : []), ...(opts.restoreAskUserQuestion true ? [--restore-ask-user-question] : []), ]; return extraArgs.length 0 ? extraArgs : undefined; }对应的测试acp-child-extra-args.test.ts、run-qwen-serve.test.ts、server-acp-child-extra-args.test.ts验证了--restore-ask-user-question与--experimental-lsp一起合并转发到 ACP child且 fast-path 上也保留该标志fast-path.ts。为什么不能复用continueLastTurn一个直觉方案是让用户通过通用的继续上一轮来回答但设计文档明确否定了这条路通用的 continue 机制会把悬挂的functionCall分类为interrupted_turn中断轮然后合成一条错误函数响应——把问题恢复成工具崩溃若照此处理HITL 交互就彻底丢失了。因此恢复restore是一条独立被跟踪的 prompt 路径当开关开启且尾部问题可恢复时continueLastTurn会主动拒绝返回accepted: false, interruption: none而不是用合成失败去回答恢复的问题。也就是说恢复与 continue 在调度入口处就分道扬镳绝不允许两条路径互相污染。运行时五步调用链设计文档将恢复的完整生命周期拆成五步每一步都有明确的源码落点第 1 步startChat 选择性跳过 orphan repairstartChat在初始化历史时执行orphan_tool_use_repairclient.ts其注释解释了背景进程在processStreamResponse推送 partial tool_use 与 React 调度器提交 tool_result 之间崩溃时会在 JSONL 中留下悬挂的model[functionCall]若不修复resume 后的首次 API 调用会因tool_use_id ... corresponding tool_use报 400。恢复路径的关键在于当getRestoreAskUserQuestion()或getPreserveRestorableAskUserQuestion()为真时先通过restorableAskUserQuestionCallIds(chat.peekLastHistoryEntry())算出可恢复的 call id 集合再以preserveCallIds参数跳过对这些 call 的修复。而被抑制的恢复无 client、fork 恢复则与 replay finalization 保持步调一致地修复 Gemini 历史——通过Config.suppressRestorableAskUserQuestionPreservation()把 preserve 标志关掉config.ts。此外per-send 内联修复通道始终会关闭悬挂调用一个普通 prompt 若先于恢复 prompt 到达绝不能发送model[functionCall] → user[text]的形状Anthropic 兼容 provider 会拒绝。而恢复自身发送的是真实 functionResponse因此该通道在恢复路径上是 no-op。第 2 步transcript replay 跳过 finalizeTranscript replay 的finalize()会跳过这些 call id使 UI 保持 in-progress 状态。skip ids 有两个来源live chat 已初始化时来自chat.peekLastHistoryEntry()的实时扫描冷批量加载historyReplay: response在startChat之前运行时来自 transcript 尾部的lastHistoryContentFromRecords——该函数从记录数组末尾向前跳过system类型记录返回最后一条 API 可见内容ask-user-question-restore.ts。skip 与 re-hang 必须保持步调一致当 daemon 已知会拒绝恢复无附加 client、fork 恢复时child 绑定的请求会携带qwen.daemon.suppressRestoreAskUserQuestionchild 既不提示也不跳过replay 照常把问题 finalize 为失败。只读的qwen/session/loadUpdates始终 finalize从不 re-hang。第 3 步child load/resume_meta携带恢复提示当 argv 开关开启且会话符合资格时child 的 load/resume_meta会包含qwen.daemon.restoreAskUserQuestion。默认关闭的路径永远不会调用恢复专用的 Session helper。这里的preserveRestorableAskUserQuestion字段config.ts默认跟随restoreAskUserQuestion并在会真正 re-hang 的 load/resume 中保持打开。第 4 步bridge 准入与空闲闸门bridge 只有在同时看到提示和 daemon 开关时才准入一个带该 meta 的 trackedsendPrompt准入方式与 continue 相同。安全边界包括外部POST /prompt无法伪造/夹带该 meta准入还要求入口在触发时处于空闲状态promptActive、pendingPromptCount、goalTurnActive必须全部清零准入失败只记日志并被吞掉——恢复是成功 load 的 best-effort 副作用不阻塞加载本身。第 5 步Session.prompt() 重建工具并写回真实响应Session.prompt()依次完成重建工具 →requestPermission→ Submit 把真实函数响应写回 → 模型在同一轮继续执行。细节包括恢复续跑跳过 file-history 快照它们不是用户轮次挂起的 worktree / recovered-agent 通知附加到回答后的消息上且只有在该消息落地后才清除Cancel 会持久化一次 declinelive decline 处理无人值守终止timeout、session_closed、abort对整个批次不持久化任何东西调用保持悬挂在 transcript 中后续 load 可以再次 re-hang——这正是恢复可重试的容错设计。状态可见性与投票路由的复用恢复完成后会话重新进入等待状态。会话列表路由把 live 状态投影到响应中routes/session.tsisWaitingForUserQuestion: session.isWaitingForUserQuestion ?? false,GET /session/:id/status同样恢复isWaitingForUserQuestion与pendingInteractions的可见性standalone-session-service.ts 对 live 状态做了投影。投票路由本身无需任何改动——POST /session/:id/permission/:requestIdroutes/permission.ts解析请求体与 client id 头通过respondToSessionPermission把答案交给 bridge。权限策略由PermissionMediator按first-responder首个有效投票胜出当前默认、designated仅发起者可答、consensusN-of-M 仲裁、local-only仅回环客户端四种契约裁决permission.ts。恢复只是以新 requestId 重新进入这套既有流程因此权限审计环与旧 requestId 一样不跨重启持久化。边界与明确不做的事Out of scope设计文档划定了严格的边界防止功能蔓延不做daemon 启动时的自动恢复boot-time auto-resume——恢复永远绑定客户端 load/resume 动作不做exec/edit 或其他非 AUQ 权限类型的恢复不做AUQ 与其他悬挂工具混合批次的恢复——混合批次整体走 orphan repair不改变通用continueLastTurn的合成失败语义不持久化旧requestId或 permission audit ring。小结--restore-ask-user-question是 qwen-code daemon 为 HITL 交互补齐的最后一块拼图它让daemon 重启从一次静默丢失用户问题的故障变成一次可重试的挂起恢复。整个方案的工程要点可归纳为纯历史扫描判资格findRestorableAskUserQuestion只读最后一条 model 轮零成本、fail-closed双通道步调一致orphan repair 的 preserve 与 replay 的 finalize skip 必须锁步任何一侧失衡都会造成历史形状损坏或 UI 状态错乱准入严格client id 必须存在、会话必须空闲、外部POST /prompt无法夹带 meta可重试容错无人值守终止不持久化任何东西调用保持悬挂下次 load 可再次恢复。若需在真实环境中启用请在 daemon 侧qwen serve --restore-ask-user-question与 ACP child 侧qwen --acp --restore-ask-user-question同时配置且牢记该标志在纯 TUI 模式下会被忽略——这是保证恢复会话不被卡死的设计底线。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考