Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/development/bugs/event-sourced-updated-at-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Resolved: `_todo_from_added_event` 丢弃 `updated_at`

**状态**:已修复(PR #3359,回归测试已转绿)。

## 问题

`_todo_from_added_event` 的构造字典漏拷贝 `updated_at`。生产端 `event_writeback.py` 在
`TODO_ADDED` payload 中始终写入该字段,读取端 `render_todo_markdown` 与
`management_projection` 又消费它;reducer 漏拷贝使新增 todo 投影丢失最后活动时间,
破坏「生产者写入 → 投影 → 消费者读取」的保真不变量。

## 修复

构造字典补入 `"updated_at": compact_text(payload.get("updated_at"))`,归一化与完成事件
reducer 一致。缺省时 `compact_text(None)` 为空串,读取端
`todo.get("updated_at") or todo.get("latest_event_at")` 的 fallback 不变。

## 回归测试

`test_updated_at_survives_todo_add_projection`
(`tests/control_plane/test_todo_mutation_authority.py`)—— 走真实 `TODO_ADDED` 事件 +
`build_state_projection`,断言投影保留时间戳。
1 change: 1 addition & 0 deletions loopx/event_sourced_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ def _todo_from_added_event(event: dict[str, Any]) -> dict[str, Any]:
"planner_order": payload.get("planner_order"),
"append_sequence": event.get("append_sequence"),
"last_event_id": event.get("event_id"),
"updated_at": compact_text(payload.get("updated_at")),
}
if task_class:
todo["task_class"] = task_class
Expand Down
22 changes: 22 additions & 0 deletions tests/control_plane/test_todo_mutation_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,28 @@ def test_task_domain_survives_markdown_event_projection() -> None:
assert projection["agent_todos"]["items"][0]["task_domain"] == "validation"


def test_updated_at_survives_todo_add_projection() -> None:
added = make_state_event(
event_id="evt-updated-at-add",
goal_id=GOAL_ID,
event_type=TODO_ADDED,
refs={"todo_id": "todo_updated_at001"},
payload={
"role": "agent",
"title": "Validate one adaptive lane.",
"updated_at": "2026-07-18T00:00:00+00:00",
},
recorded_at="2026-07-18T00:00:00+00:00",
)

projection = build_state_projection([added])

assert (
projection["agent_todos"]["items"][0]["updated_at"]
== "2026-07-18T00:00:00+00:00"
)


def test_task_domain_event_update_is_normalized_and_invalid_values_fail() -> None:
added = make_state_event(
event_id="evt-domain-add",
Expand Down
Loading