Skip to content

fix(core): preserve active Todo context across tool turns - #7915

Closed
yiliang114 wants to merge 1 commit into
QwenLM:agent/pr-6945-hardeningfrom
yiliang114:cx/preserve-active-todo-context
Closed

fix(core): preserve active Todo context across tool turns#7915
yiliang114 wants to merge 1 commit into
QwenLM:agent/pr-6945-hardeningfrom
yiliang114:cx/preserve-active-todo-context

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

This change keeps the latest unfinished Todo list salient across tool turns. After a successful Todo update, Qwen Code retains a bounded prompt-scoped reminder and appends it after function responses in both the core and ACP loops. Mid-turn user input remains last so the user's newest direction keeps precedence.

Todo reminders are isolated by work-chain prompt ID, so cron jobs and background notifications cannot overwrite an ordinary user task. Retry, continue, and explicitly related automatic turns move the reminder to their new prompt ID, while session changes clear it. The existing experimental Todo Stop Guard remains unchanged and disabled by default.

Why it's needed

Today the complete Todo state is emphasized only in the immediate todo_write result. After several other tool calls, that state loses salience and the model can naturally end the turn while Todo items are still pending. Re-reading the persisted Todo file is not safe because it can outlive the work chain that created it, and changing stop semantics would turn a planning aid into a completion oracle.

This preserves the current task context before the stop decision without treating Todo state as proof that the task is complete or forcing an automatic retry.

Reviewer Test Plan

How to verify

  1. Start a multi-step task that creates a pending Todo and performs at least one additional tool call. Confirm the next model request contains the unfinished Todo reminder after the tool's function response.
  2. Complete every Todo and perform another tool call. Confirm the reminder is absent.
  3. Start an unrelated user prompt. Confirm the previous reminder is cleared.
  4. Retry or continue the interrupted task. Confirm its reminder follows the continued work chain.
  5. Run a cron or background-notification tool loop alongside an ordinary task. Confirm each loop reads only the reminder owned by its prompt ID.
  6. Queue mid-turn user input while a tool executes. Confirm the request order is function responses, Todo reminder, then the user's new input.

Evidence (Before & After)

N/A — model-context behavior with no UI change. Focused regressions cover Todo persistence, prompt ownership, Core and ACP injection order, retry/continue transfer, and the Todo Stop Guard remaining off by default.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Local Node.js 22 workspace. npm run build, npm run typecheck, changed-file ESLint and Prettier, TodoWrite and Config tests, Core client and scheduler tests, non-interactive CLI tests, and focused ACP Session regressions all pass. A full ACP Session run reached 454 passing tests and one pre-existing 30-second mid-turn drain timeout; the affected ACP tests pass when run directly.

Risk & Scope

  • Main risk or tradeoff: active Todo reminders add a small amount of repeated context on tool-result turns; only unfinished items are included, the payload is capped at 4,000 characters, and prompt ownership bounds its lifetime.
  • Not validated / out of scope: changing natural-stop semantics, enabling or removing todoStopGuard, and guaranteeing that every model will always follow the reminder.
  • Breaking changes / migration notes: none.

Linked Issues

Related to #6945 and #7821.

中文说明

这个 PR 做了什么

这个改动让最新的未完成 Todo 列表在连续工具轮次中保持显著。todo_write 成功后,Qwen Code 会保留一段有大小上限、按 prompt 隔离的 reminder,并在 Core 和 ACP 循环中把它追加到 function response 之后。轮次中途到达的用户输入仍然放在最后,因此用户最新指令保持最高优先级。

Todo reminder 按工作链 prompt ID 隔离,因此 cron 任务和后台 notification 不会覆盖普通用户任务。retry、continue 和明确相关的自动轮次会把 reminder 转移到新的 prompt ID;session 切换会清理 reminder。现有实验性 Todo Stop Guard 不做修改,默认仍然关闭。

为什么需要

当前完整 Todo 状态只会在当次 todo_write 结果中被重点提示。经过若干其他工具调用后,这段状态的显著性会降低,模型可能在仍有 pending Todo 时自然结束轮次。直接重新读取持久化 Todo 文件并不安全,因为文件可能比创建它的工作链存活更久;而修改 stop 语义则会把规划辅助误当成完成判定器。

这个方案在模型做出停止决定之前持续保留当前任务上下文,但不会把 Todo 状态当作任务已经完成的证据,也不会强制自动重试。

Reviewer 测试计划

如何验证

  1. 启动一个会创建 pending Todo、并至少继续执行一次其他工具调用的多步骤任务。确认下一次模型请求在工具 function response 之后包含未完成 Todo reminder。
  2. 完成所有 Todo 后再执行一次工具调用。确认 reminder 不再出现。
  3. 启动一个无关的新用户 prompt。确认旧 reminder 已清理。
  4. retry 或 continue 被中断的任务。确认 reminder 跟随续接后的工作链。
  5. 在普通任务旁运行 cron 或后台 notification 工具循环。确认每个循环只读取自己 prompt ID 对应的 reminder。
  6. 工具执行期间加入一条中途用户输入。确认请求顺序是 function response、Todo reminder、用户新输入。

证据(Before & After)

N/A —— 这是模型上下文行为变化,没有 UI 改动。聚焦回归覆盖 Todo 持久化、prompt ownership、Core 与 ACP 注入顺序、retry/continue 转移,以及 Todo Stop Guard 默认仍关闭。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

本地 Node.js 22 workspace。npm run buildnpm run typecheck、变更文件 ESLint 与 Prettier、TodoWrite 与 Config 测试、Core client 与 scheduler 测试、non-interactive CLI 测试和 ACP Session 聚焦回归均通过。ACP Session 全量运行有 454 个测试通过,另有一个既有的 mid-turn drain 用例在 30 秒超时;本次影响到的 ACP 测试单独运行均通过。

风险与范围

  • 主要风险或取舍:active Todo reminder 会在 tool-result 轮次加入少量重复上下文;内容只包含未完成项、上限为 4,000 字符,并通过 prompt ownership 限制生命周期。
  • 未验证 / 不在本次范围:修改自然停止语义、开启或移除 todoStopGuard,以及保证所有模型始终遵循 reminder。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

#6945#7821 相关。

@doudouOUC
doudouOUC deleted the branch QwenLM:agent/pr-6945-hardening July 28, 2026 07:49
@doudouOUC doudouOUC closed this Jul 28, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

⚠️ Base branch issue: this PR targets agent/pr-6945-hardening, which no longer exists in QwenLM/qwen-code (the branch from merged PR #6945). GitHub reports the PR as CONFLICTING/DIRTY and no CI has run. The base needs to be changed to main before this can land.

Problem: the problem is real and observed — after several tool calls following a todo_write, the todo state loses salience and the model can end the turn with unfinished items. This is the same motivation behind #6945 (Todo Stop Guard) and #7821, and this PR takes a complementary approach: preserving context before the stop decision rather than guarding after it.

Direction: aligned. Keeping task context salient across tool turns is squarely within the agent's core mission. The approach (prompt-scoped reminder injected after function responses) is a standard context-management technique and doesn't change stop semantics.

Size: 190 production lines (Session.ts 70, config.ts 40, coreToolScheduler.ts 27, todoWrite.ts 22, client.ts 22, nonInteractiveCli.ts 9), 275 test lines, 33 docs. Not applicable for escalation.

Approach: the scope feels right — the feature needs to touch both loops (core + ACP), the config layer, the tool, and the scheduler. Good reuse of the existing promptIdContext and escapeSystemReminderTags. Two things worth discussing:

  1. Core vs ACP work-chain clearing: in client.ts, Cron/Notification/Teammate turns call startActiveTodoWorkChain(prompt_id) which clears all reminders. In Session.ts, the same turns use the selective startAutomaticActiveTodoWorkChain. This means a cron turn in the non-interactive CLI would wipe the user's active todo reminder, while the daemon loop preserves it. Is the blunt clearing intentional for the core loop?

  2. Todo Stop Guard infrastructure changes: the PR always captures the background baseline (not just when the guard is enabled) and adds a #resetTodoStopGuardBackgroundLineage() call on non-continuing turns even when the guard is off. The description says "The existing experimental Todo Stop Guard remains unchanged" — the guard's stop-blocking logic is unchanged, but its supporting state management is modified. Worth noting in the description.

Risk: packages/cli/src/acp-integration/session/Session.ts matches the high-risk path pattern (acp-integration). Full CI evidence is required before approval. The base branch issue currently prevents any CI from running.

Flagging the base branch and the two design questions above before diving deeper.

中文说明

感谢贡献!

模板完整 ✓

⚠️ 基础分支问题: 此 PR 的目标分支是 agent/pr-6945-hardening,该分支在 QwenLM/qwen-code 中已不存在(来自已合并的 PR #6945)。GitHub 显示 PR 状态为 CONFLICTING/DIRTY,且没有 CI 运行。需要将基础分支改为 main 才能合入。

问题:问题是真实且可观察的——在 todo_write 之后经过多次工具调用,todo 状态的显著性会降低,模型可能在仍有未完成项时结束轮次。这与 #6945(Todo Stop Guard)和 #7821 的动机一致,本 PR 采取了互补方案:在停止决定之前保留上下文,而非在之后进行守护。

方向:对齐。在连续工具轮次中保持任务上下文显著性完全属于 agent 的核心使命。方案(按 prompt 隔离的 reminder 注入到 function response 之后)是标准的上下文管理技术,不改变停止语义。

规模:190 行生产代码(Session.ts 70、config.ts 40、coreToolScheduler.ts 27、todoWrite.ts 22、client.ts 22、nonInteractiveCli.ts 9),275 行测试,33 行文档。不需要升级。

方案:范围合理——功能需要触及两个循环(core + ACP)、config 层、工具和调度器。良好复用了现有的 promptIdContextescapeSystemReminderTags。有两点值得讨论:

  1. Core 与 ACP 工作链清理不一致: client.ts 中 Cron/Notification/Teammate 轮次调用 startActiveTodoWorkChain(prompt_id) 会清除所有 reminder。Session.ts 中相同轮次使用选择性的 startAutomaticActiveTodoWorkChain。这意味着非交互式 CLI 中的 cron 轮次会清除用户的 active todo reminder,而 daemon 循环会保留。Core 循环的整体清理是有意为之吗?

  2. Todo Stop Guard 基础设施变更: PR 始终捕获后台基线(不仅在 guard 启用时),并在非续接轮次时即使 guard 关闭也调用 #resetTodoStopGuardBackgroundLineage()。描述中说"现有实验性 Todo Stop Guard 保持不变"——guard 的停止阻断逻辑确实未变,但其支撑状态管理被修改了。建议在描述中注明。

风险:packages/cli/src/acp-integration/session/Session.ts 匹配高风险路径模式(acp-integration)。批准前需要完整的 CI 证据。基础分支问题目前阻止了任何 CI 运行。

先提出基础分支问题和上述两个设计问题,再深入看代码。

Qwen Code · qwen3.8-max-preview

Reviewed at 48865472657725b75075b5ad3b846d7b28de136f · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I'd store unfinished todos in a session-scoped map keyed by prompt ID, inject the reminder after function responses in both loops (core + ACP), clear on completion or new work chain, transfer on retry/continue, and cap the payload. The PR's approach matches this closely.

Assessment: the implementation is clean and well-structured. Good reuse of the existing promptIdContext (AsyncLocalStorage) and escapeSystemReminderTags. The getOwnActiveTodoReminders() pattern correctly handles child Configs created via Object.create(parent) — each config level gets its own Map without polluting the prototype chain. The 4,000-char cap with [truncated] marker is a reasonable bound. Test coverage is thorough at 275 lines, covering prompt ownership, work-chain transfer, child-config isolation, session clearing, injection ordering, and the truncation bound.

Findings:

  1. Core vs ACP work-chain clearing (non-blocking): in client.ts, Cron/Notification/Teammate turns call startActiveTodoWorkChain(prompt_id) which clears all reminders. In Session.ts, the same turns use the selective startAutomaticActiveTodoWorkChain that only clears the continued-from and target entries. A cron turn in the non-interactive CLI would wipe an unrelated user todo reminder; the daemon loop preserves it. If the blunt clearing is intentional for the simpler core loop, a short comment would help future readers.

  2. Todo Stop Guard infrastructure (non-blocking): the baseline capture is now unconditional (previously gated on todoStopGuardEnabled), and #resetTodoStopGuardBackgroundLineage() runs on non-continuing turns even when the guard is off. The guard's stop-blocking logic is unchanged, but the supporting state management is modified — the PR description's "remains unchanged" could be more precise.

  3. promptIdContext.enterWith in #runTools (non-blocking): Session.ts uses enterWith(promptId) at the top of #runTools, while coreToolScheduler.ts wraps each invocation.execute() in promptIdContext.run(). The run wrapper already provides the prompt ID for tool executions, so the enterWith is redundant for that path — but it covers any non-scheduler code within #runTools that might read the context. Not harmful, just worth knowing.

No critical blockers found in the code itself.

Testing

Check Conclusion
(no CI runs) base branch agent/pr-6945-hardening does not exist in QwenLM/qwen-code — no pull_request event workflow has triggered

No unit, build, or lint CI has run on this commit. The only workflow runs are pull_request_target orchestration jobs (triage, review, label). The PR's own CI suite cannot trigger until the base branch is changed to main.

Not verified: build, typecheck, unit tests, lint — no CI evidence available. The author reports local passes (npm run build, npm run typecheck, focused tests), but this is the author's claim, not verified evidence.

中文说明

代码审查

独立方案: 我会将未完成 todo 存储在按 prompt ID 键控的会话级 Map 中,在两个循环(core + ACP)的 function response 之后注入 reminder,在完成或新工作链时清除,在 retry/continue 时转移,并限制载荷大小。PR 的方案与此高度一致。

评估: 实现干净、结构良好。良好复用了现有的 promptIdContext(AsyncLocalStorage)和 escapeSystemReminderTagsgetOwnActiveTodoReminders() 模式正确处理了通过 Object.create(parent) 创建的子 Config——每个 config 层级拥有自己的 Map,不会污染原型链。4,000 字符上限加 [truncated] 标记是合理的限制。测试覆盖充分,共 275 行,涵盖 prompt 所有权、工作链转移、子 config 隔离、session 清理、注入顺序和截断限制。

发现:

  1. Core 与 ACP 工作链清理不一致(非阻塞): client.ts 中 Cron/Notification/Teammate 轮次调用 startActiveTodoWorkChain(prompt_id) 清除所有 reminder。Session.ts 中相同轮次使用选择性的 startAutomaticActiveTodoWorkChain,只清除续接来源和目标条目。非交互式 CLI 中的 cron 轮次会清除无关的用户 todo reminder;daemon 循环则保留。如果简单 core 循环的整体清理是有意为之,加一行简短注释会有助于后续读者理解。

  2. Todo Stop Guard 基础设施(非阻塞): 基线捕获现在是无条件的(之前受 todoStopGuardEnabled 门控),且 #resetTodoStopGuardBackgroundLineage() 在 guard 关闭时也会在非续接轮次运行。Guard 的停止阻断逻辑未变,但支撑状态管理被修改——PR 描述中的"保持不变"可以更精确。

  3. #runTools 中的 promptIdContext.enterWith(非阻塞): Session.ts 在 #runTools 顶部使用 enterWith(promptId),而 coreToolScheduler.ts 将每个 invocation.execute() 包装在 promptIdContext.run() 中。run 包装已经为工具执行提供了 prompt ID,因此 enterWith 对该路径是冗余的——但它覆盖了 #runTools 中可能读取上下文的非调度器代码。无害,仅须知晓。

代码本身未发现关键阻塞问题。

测试

没有 CI 运行。基础分支 agent/pr-6945-hardening 在 QwenLM/qwen-code 中不存在,pull_request 事件工作流未触发。唯一的 workflow run 是 pull_request_target 编排任务(triage、review、label)。在基础分支改为 main 之前,PR 自身的 CI 套件无法触发。

未验证:构建、类型检查、单元测试、lint——无 CI 证据。作者报告本地通过(npm run buildnpm run typecheck、聚焦测试),但这是作者声明,非验证证据。

Qwen Code · qwen3.8-max-preview

Reviewed at 48865472657725b75075b5ad3b846d7b28de136f · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the code is clean and the approach is right, but the PR targets a non-existent base branch and has zero CI coverage, so it cannot be verified or merged as-is.

Stepping back: the design is sound. Prompt-scoped todo reminders injected after function responses is the right mechanism — it preserves context without changing stop semantics, and the work-chain isolation prevents cross-contamination between concurrent turns. The implementation reuses existing infrastructure well (promptIdContext, escapeSystemReminderTags), handles the prototype-chain Config model correctly, and the test suite is thorough. If I had to maintain this in six months, I'd be fine with it.

What holds this back from approval:

  1. Base branch agent/pr-6945-hardening does not exist in QwenLM/qwen-code. GitHub reports CONFLICTING/DIRTY. The PR needs to be retargeted to main.
  2. No CI has run — no unit tests, build, lint, or typecheck evidence exists for this commit. The pull_request workflow cannot trigger against a missing base branch.
  3. Two non-blocking design questions from Stage 1 remain open (core vs ACP work-chain clearing asymmetry; todo stop guard description precision).

Once the base is fixed and CI runs green, this should be straightforward to approve. The code itself has no critical blockers.

⏸️ Not approving — the base branch and CI gaps need resolution first. @yiliang114 could you retarget this to main?

中文说明

置信度:3/5 —— 代码干净、方案正确,但 PR 的目标分支不存在且没有 CI 覆盖,无法验证或合入。

总体来看:设计合理。按 prompt 隔离的 todo reminder 注入到 function response 之后是正确的机制——保留上下文而不改变停止语义,工作链隔离防止了并发轮次之间的交叉污染。实现良好复用了现有基础设施(promptIdContextescapeSystemReminderTags),正确处理了原型链 Config 模型,测试套件充分。如果六个月后需要维护这段代码,我不会有问题。

阻止批准的原因:

  1. 基础分支 agent/pr-6945-hardening 不存在于 QwenLM/qwen-code。GitHub 显示 CONFLICTING/DIRTY。需要将 PR 目标改为 main
  2. 没有 CI 运行 —— 此提交没有单元测试、构建、lint 或类型检查证据。pull_request 工作流无法对不存在的目标分支触发。
  3. Stage 1 中的两个非阻塞设计问题仍未解决(core 与 ACP 工作链清理不对称;todo stop guard 描述精确度)。

基础分支修复并且 CI 绿色通过后,批准应该是顺理成章的。代码本身没有关键阻塞问题。

⏸️ 暂不批准——需要先解决基础分支和 CI 问题。@yiliang114 能否将目标分支改为 main

Qwen Code · qwen3.8-max-preview

Reviewed at 48865472657725b75075b5ad3b846d7b28de136f · re-run with @qwen-code /triage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants