feat: implement todo plan clearance on turn start
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-29-todo-write-tool.md: 760373d64f462e3717a174d5793e6d47ab76b0b4
|
||||
2026-06-29-todo-write-tool.zh.md: fd29e6049e6a729c2c7e78860ad7c065422da60e
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-29-todo-write-tool.md
|
||||
2026-06-29-todo-write-tool.md: 288932f641a37c13ea6beeb069ac360c4a8447c1
|
||||
2026-06-29-todo-write-tool.zh.md: 03dae6328e5c7f4379694ab01db3434b4469826c
|
||||
|
||||
@@ -18,7 +18,7 @@ The model sends the entire list every call; the new list replaces the old (last-
|
||||
|
||||
### State on the session log, not a service
|
||||
|
||||
The list is appended as a `todo/write` event carrying the full `{ todos }` snapshot. The harness is event-sourced — the LLM history, tool calls, and turn structure all live on the log — so the todo list lives there too. This buys durability, replay, and resume reconstruction for free: a reopened session re-derives the current list from the latest `todo/write`, with no separate persistence backend, in-memory service to rehydrate, or extra wiring. An in-memory `ctx.todos` service would have to reinvent all of that. (Full-log consumers get this reconstruction outright; the web client's paged window gets it from the tail history page's host-computed projection — see the [web todo display note](2026-07-23-web-todo-display.md).)
|
||||
The list is appended as a `todo/write` event carrying the full `{ todos }` snapshot. The harness is event-sourced — the LLM history, tool calls, and turn structure all live on the log — so the todo list lives there too. This buys durability, replay, and resume reconstruction for free: a reopened session re-derives the standing plan from the latest `todo/write` that is not followed by a later `turn/start` ([plan strip lifetime](2026-07-28-todo-plan-clears-on-next-turn.md)), with no separate persistence backend, in-memory service to rehydrate, or extra wiring. An in-memory `ctx.todos` service would have to reinvent all of that. (Full-log consumers get this reconstruction outright; the web client's paged window gets it from the tail history page's host-computed projection — see the [web todo display note](2026-07-23-web-todo-display.md).)
|
||||
|
||||
### NOT a surface event
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ harness 为模型提供了 bash 和 subagent 工具,却没有办法记录结
|
||||
|
||||
### 状态在会话日志上,而非服务
|
||||
|
||||
列表作为 `todo/write` 事件追加到日志,携带完整的 `{ todos }` 快照。harness 是事件溯源的——LLM(大语言模型)历史、工具调用和轮次结构都在日志上——所以 todo 列表也在那里。这免费获得了持久性、回放和恢复重建:重新打开的会话从最新的 `todo/write` 重新推导当前列表,无需独立的持久化后端、无需重新注水的内存服务、无需额外接线。一个内存中的 `ctx.todos` 服务需要重新发明以上所有。(全量 log 消费者直接获得这份重建;web 客户端的分页窗口则从尾页 history 携带的 host 计算投影获得——见 [web todo 展示 Note](2026-07-23-web-todo-display.md)。)
|
||||
列表作为 `todo/write` 事件追加到日志,携带完整的 `{ todos }` 快照。harness 是事件溯源的——LLM(大语言模型)历史、工具调用和轮次结构都在日志上——所以 todo 列表也在那里。这免费获得了持久性、回放和恢复重建:重新打开的会话从「其后没有更晚 `turn/start`」的最近一次 `todo/write` 重新推导站立计划([计划条生命周期](2026-07-28-todo-plan-clears-on-next-turn.md)),无需独立的持久化后端、无需重新注水的内存服务、无需额外接线。一个内存中的 `ctx.todos` 服务需要重新发明以上所有。(全量 log 消费者直接获得这份重建;web 客户端的分页窗口则从尾页 history 携带的 host 计算投影获得——见 [web todo 展示 Note](2026-07-23-web-todo-display.md)。)
|
||||
|
||||
### 不是 surface 事件
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-23-web-todo-display.md
|
||||
2026-07-23-web-todo-display.md: 5fe08cc40c1d23ff3a9b8c6d766fea6d3694c30d
|
||||
2026-07-23-web-todo-display.zh.md: c121ffc27e3d0a93707c2c22b2f180023ebae5be
|
||||
2026-07-23-web-todo-display.md: 7223ff9adbf1fa6dca39c9eb4949b6d3861bdd6d
|
||||
2026-07-23-web-todo-display.zh.md: ac773a3c587183aa9a152caff2812686bc79f3c7
|
||||
|
||||
@@ -14,7 +14,7 @@ Consume `todo/write` as a Session side effect, not a surface node, and render it
|
||||
|
||||
### Side-effect channel, converging with window replay
|
||||
|
||||
`applyEventSideEffects` gains a `todo/write` case (whole list, last write wins). Unlike partial/openCalls, `rebuildDerivedFromWindow` deliberately does NOT reset it: the value is session-level — taken from the tail history page's full-log projection — and an arbitrary window may not contain the latest write, so an older-page prepend keeps it and only an in-window or live write overwrites it. Every `installWindow` caller is a tail request (`doOpen`, its gap re-pull, `repairGap`; `loadOlder` prepends without it), which the host answers with the projection or omits it only when the full log holds no `todo/write` — so an absent field is the authoritative empty list and is assigned as such. That distinction matters on rollback: a live write whose host crashed before persisting leaves the log empty, and preserving the prior value instead would strand the rolled-back plan on screen indefinitely. `ConversationSnapshot.todos` is the read surface. This follows the event's own contract ("log-only UI state; never derived history"): surfacing each write as a conversation node would render superseded lists as if they were still standing.
|
||||
`applyEventSideEffects` gains a `todo/write` case (whole list, last write wins) and clears on `turn/start` ([turn-scoped plan lifetime](2026-07-28-todo-plan-clears-on-next-turn.md)). `rebuildDerivedFromWindow` sweeps the window from an empty plan and restores the tail-page seed only when the window never determined the plan (no `todo/write` and no `turn/start`); otherwise the in-window write/`turn/start` fold wins. Every `installWindow` caller is a tail request (`doOpen`, its gap re-pull, `repairGap`; `loadOlder` prepends without reseeding), which the host answers with the projection or omits it when no plan stands — so an absent field is the authoritative empty list and is assigned as such. That distinction matters on rollback: a live write whose host crashed before persisting leaves the log empty, and preserving the prior value instead would strand the rolled-back plan on screen indefinitely. `ConversationSnapshot.todos` is the read surface. This follows the event's own contract ("log-only UI state; never derived history"): surfacing each write as a conversation node would render superseded lists as if they were still standing.
|
||||
|
||||
### TodoPanel: the durable list as a persistent strip
|
||||
|
||||
@@ -33,4 +33,4 @@ The dedicated `todo_write` chat row is a plain registrant plugin (`todoToolview`
|
||||
|
||||
## Consequences
|
||||
|
||||
Replay correctness is owned by one code path: any future change to window rebuild keeps todos consistent for free, and the fixture (fx-alpha turn 65) plus the assembled keyless snapshot (`apps/web/tests/todo-display.snapshot.ts`) pin the full chain (row summary and state, dock panel content, collapse round-trip) over the built client graph. `todos` is a required `ConversationSnapshot` field, so scripted fakes in specs must carry it. The TUI panel is untouched (the automation-only ACP bridge deliberately omits todo presentation); the web surfaces render the same event, adding one wire field and no new event type. That field is how cold-load reconstruction stays host-backed: the tail history page carries `todos` — the full-log latest `todo/write`, computed independently of the page window (the same backscan posture the view pairing uses) — so a reopened session restores the plan even when the last write precedes the window; that value survives an older-page prepend, is overwritten by any later write, and resets to empty when a tail response carries no projection.
|
||||
Replay correctness is owned by one code path: any future change to window rebuild keeps todos consistent for free, and the fixture (fx-alpha turn 65) plus the assembled keyless snapshot (`apps/web/tests/todo-display.snapshot.ts`) pin the full chain (row summary and state, dock panel content, collapse round-trip) over the built client graph. `todos` is a required `ConversationSnapshot` field, so scripted fakes in specs must carry it. The TUI panel shares the same turn-scoped lifetime (the automation-only ACP bridge deliberately omits todo presentation); the web surfaces render the same event, adding one wire field and no new event type. That field is how cold-load reconstruction stays host-backed: the tail history page carries `todos` — the full-log standing plan (latest `todo/write` with no later `turn/start`), computed independently of the page window (the same backscan posture the view pairing uses) — so a reopened session restores the plan when it still stands and the last write precedes the window; that value survives an older-page prepend, is overwritten by any later write, clears on a later `turn/start`, and resets to empty when a tail response carries no projection.
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
### 副作用通道,与窗口回放收敛
|
||||
|
||||
`applyEventSideEffects` 新增一个 `todo/write` 分支(整份列表,后写覆盖先写)。与 partial/openCalls 不同,`rebuildDerivedFromWindow` 刻意不重置它:该值是会话级的——取自尾页 history 携带的全量 log 投影——而任意窗口未必包含最近一次写入,因此往前翻页保留它,只有窗口内或实时的写入才会覆盖。`installWindow` 的每个调用方都是尾页请求(`doOpen`、其补洞重拉、`repairGap`;`loadOlder` 只往前拼接、不走它),而 host 对尾页请求要么带上投影、要么仅在全量 log 没有任何 `todo/write` 时省略——因此字段缺失就是权威的空列表,直接照此赋值。这个区分在回滚场景上要紧:实时写入若在 host 持久化前崩溃,log 里就是空的,此时保留旧值会让已回滚的计划永远留在屏幕上。`ConversationSnapshot.todos` 是读取面。这遵循事件自身的契约(「仅日志 UI 状态,绝非派生历史」):把每次写入作为对话节点呈现,会让已被取代的列表看起来仍然有效。
|
||||
`applyEventSideEffects` 新增一个 `todo/write` 分支(整份列表,后写覆盖先写),并在 `turn/start` 清空([按 turn 界定的计划生命周期](2026-07-28-todo-plan-clears-on-next-turn.md))。`rebuildDerivedFromWindow` 从空计划扫过窗口,仅当窗口从未判定计划(无 `todo/write` 且无 `turn/start`)时恢复尾页种子;否则以窗口内写入/`turn/start` 折叠为准。`installWindow` 的每个调用方都是尾页请求(`doOpen`、其补洞重拉、`repairGap`;`loadOlder` 只往前拼接、不再播种),而 host 对尾页请求要么带上投影、要么在无站立计划时省略——因此字段缺失就是权威的空列表,直接照此赋值。这个区分在回滚场景上要紧:实时写入若在 host 持久化前崩溃,log 里就是空的,此时保留旧值会让已回滚的计划永远留在屏幕上。`ConversationSnapshot.todos` 是读取面。这遵循事件自身的契约(「仅日志 UI 状态,绝非派生历史」):把每次写入作为对话节点呈现,会让已被取代的列表看起来仍然有效。
|
||||
|
||||
### TodoPanel:长驻列表作为一条常驻横条
|
||||
|
||||
@@ -33,4 +33,4 @@ Status: implemented
|
||||
|
||||
## Consequences
|
||||
|
||||
回放正确性由一条代码路径掌管:未来对窗口重建的任何改动都免费保持 todos 一致;fx-alpha 第 65 轮的 fixture(测试前置数据)加 assembled keyless snapshot(`apps/web/tests/todo-display.snapshot.ts`)在构建产物客户端全图上钉住整条链(行摘要与状态、dock 面板内容、折叠往返)。`todos` 是 `ConversationSnapshot` 的必填字段,所以 spec 里脚本化的 fake 必须带上它。TUI 面板未受改动(自动化专用的 ACP 桥接刻意不做 todo 呈现);Web 各面渲染同一个事件,只新增一个协议字段,不新增事件类型。冷加载重建正是靠这个字段由 host 兜底:history 尾页附带 `todos`——全量 log 上最新一次 `todo/write` 的投影,独立于分页窗口计算(与 view 配对同一种 backscan 姿势)——因此重开会话时即使最后一次写入落在窗口之前,计划也照常恢复;该值跨往前翻页保留,之后的任何写入照常覆盖,而尾页响应不带投影时复位为空。
|
||||
回放正确性由一条代码路径掌管:未来对窗口重建的任何改动都免费保持 todos 一致;fx-alpha 第 65 轮的 fixture(测试前置数据)加 assembled keyless snapshot(`apps/web/tests/todo-display.snapshot.ts`)在构建产物客户端全图上钉住整条链(行摘要与状态、dock 面板内容、折叠往返)。`todos` 是 `ConversationSnapshot` 的必填字段,所以 spec 里脚本化的 fake 必须带上它。TUI 面板共用同一按 turn 界定的生命周期(自动化专用的 ACP 桥接刻意不做 todo 呈现);Web 各面渲染同一个事件,只新增一个协议字段,不新增事件类型。冷加载重建正是靠这个字段由 host 兜底:history 尾页附带 `todos`——全量 log 上的站立计划(其后没有更晚 `turn/start` 的最近一次 `todo/write`),独立于分页窗口计算(与 view 配对同一种 backscan 姿势)——因此重开会话时若计划仍站立且最后一次写入落在窗口之前,计划也照常恢复;该值跨往前翻页保留,之后的任何写入照常覆盖,更晚的 `turn/start` 会清空,而尾页响应不带投影时复位为空。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md
|
||||
2026-07-28-todo-plan-clears-on-next-turn.md: 64c7207d334689817c3a392a0866158e4c1b927d
|
||||
2026-07-28-todo-plan-clears-on-next-turn.zh.md: 9df4d952888df9e83f84deb3f2d587b5f9e2ff77
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Todo plan strip clears on the next turn
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-todo-plan-clears-on-next-turn.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`todo_write` stores whole-list snapshots on the session log, and interactive hosts render the latest list as a plan strip (web TodoPanel, TUI Plan panel). After a turn finished, that strip stayed on screen into the next user turn — a completed or abandoned checklist from the previous task. Readers treat the strip as "what this turn is doing," so a stale list across the turn boundary is the wrong product lifetime. The [web todo display](2026-07-23-web-todo-display.md) and [`todo_write` tool](2026-06-29-todo-write-tool.md) notes still own event-sourcing and the two render surfaces; they described the standing plan as lasting for the whole session until the next write.
|
||||
|
||||
## Decision
|
||||
|
||||
The standing plan is the latest `todo/write` that is not followed by a later `turn/start`. `turn/end` keeps the list visible so the finished checklist remains while the user reads the answer; the next `turn/start` clears it until the model writes again.
|
||||
|
||||
### Live path
|
||||
|
||||
Web `Session.applyEventSideEffects` and the TUI `renderEvent` switch clear the strip on `turn/start` and replace it on `todo/write`. The TUI rebuild path resets the panel before replaying the log so cold resume converges on the same rule.
|
||||
|
||||
### Cold-load / history projection
|
||||
|
||||
Host `backscanTodos` (and the fixture parallel) walks the full log from the tail: the first `turn/start` means no standing plan; the first `todo/write` is the standing list. Tail `session.history` still carries that projection (or omits it for empty). Client rebuild sweeps the window from an empty plan and restores the tail-page seed only when the window itself never determined the plan (no `todo/write` and no `turn/start`); a contiguous tail window that contains a post-write `turn/start` determines empty and matches the host.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Clear on `turn/end`** — hides the checklist while the user is still reading the just-finished answer; the strip's job at that moment is the completed plan, not an empty dock.
|
||||
- **Clear only when every item is `completed`** — leaves abandoned or partial plans across turns; the strip would still show another task's work.
|
||||
- **Append an empty `todo/write` on turn start** — mutates the log for a UI lifetime rule and invents a write the model never authored.
|
||||
|
||||
## Consequences
|
||||
|
||||
Interactive hosts and the history projection share one lifetime rule; reopening a session restores a plan only when no later turn has started. Partial supersession of the session-long standing-plan wording in [web todo display](2026-07-23-web-todo-display.md) and [`todo_write` tool](2026-06-29-todo-write-tool.md): event-sourcing, last-write-wins replacement, and the two render surfaces stay there; this note owns turn-boundary clearance. Coverage: client session specs for live clear + replay empty, host history projection after a post-write `turn/start`, and the existing web todo-display snapshot (fixture turn 65 remains the log tip, so the strip still pins).
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Todo 计划条在下一轮 turn 开始时清空
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-todo-plan-clears-on-next-turn.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`todo_write` 把整份列表快照写在会话日志上,交互式宿主把最新列表渲染为计划条(web TodoPanel、TUI Plan 面板)。一轮结束后,该条会一直留到下一个用户 turn——上一任务已完成或已放弃的清单仍挂在界面上。读者把计划条理解成「本轮在做什么」,因此跨 turn 边界的陈旧列表是错误的产品生命周期。[web todo 展示](2026-07-23-web-todo-display.md) 与 [`todo_write` 工具](2026-06-29-todo-write-tool.md) 两份 Note 仍拥有事件溯源与两个渲染面;它们曾把站立计划描述为整段会话持续到下一次写入。
|
||||
|
||||
## Decision
|
||||
|
||||
站立计划是「其后没有更晚 `turn/start`」的最近一次 `todo/write`。`turn/end` 仍保留列表,便于用户阅读刚结束的回答时对照已完成清单;下一个 `turn/start` 将其清空,直到模型再次写入。
|
||||
|
||||
### 实时路径
|
||||
|
||||
Web 的 `Session.applyEventSideEffects` 与 TUI 的 `renderEvent` 开关在 `turn/start` 清空计划条,在 `todo/write` 替换它。TUI 重建路径在回放日志前复位面板,使冷恢复收敛到同一规则。
|
||||
|
||||
### 冷加载 / history 投影
|
||||
|
||||
Host 的 `backscanTodos`(以及 fixture 平行实现)从日志尾部向前扫描:先遇到 `turn/start` 表示没有站立计划;先遇到 `todo/write` 即为站立列表。尾页 `session.history` 仍携带该投影(空则省略)。客户端重建从空计划扫过窗口,仅当窗口本身从未判定计划(无 `todo/write` 且无 `turn/start`)时恢复尾页种子;连续尾窗口若含写入之后的 `turn/start` 则判定为空,与 host 一致。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **在 `turn/end` 清空**——用户仍在阅读刚完成的回答时清单消失;此时计划条的职责正是展示完成态,而非空 dock。
|
||||
- **仅在全部 `completed` 时清空**——未完成或中途放弃的计划仍会跨 turn 残留;计划条会继续展示另一任务的工作。
|
||||
- **在 turn 开始时追加空的 `todo/write`**——为 UI 生命周期规则改写日志,并捏造模型从未写出的写入。
|
||||
|
||||
## Consequences
|
||||
|
||||
交互式宿主与 history 投影共用同一生命周期规则;重开会话仅在其后没有更新的 turn 启动时恢复计划。对 [web todo 展示](2026-07-23-web-todo-display.md) 与 [`todo_write` 工具](2026-06-29-todo-write-tool.md) 中「会话级站立计划」表述构成部分取代:事件溯源、后写覆盖与两个渲染面仍归那两份 Note;本 Note 拥有 turn 边界清空。覆盖:客户端 session 对实时清空 + 回放为空的用例、host 在写入后的 `turn/start` 之后的 history 投影,以及既有的 web todo-display 快照(fixture 第 65 轮仍是日志末端,计划条仍被钉住)。
|
||||
Reference in New Issue
Block a user