From 47ed7f6f4696556ab1e3f656405cbe569e65c7ec Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 07:20:20 +0800 Subject: [PATCH 1/7] fix: include archived completed todos in workspace history Signed-off-by: huangruiteng --- loopx/chat_completed_todos.py | 2 +- .../todos/goal_todo_projection.py | 47 +++++++++++++++---- loopx/todos.py | 19 ++++++-- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/loopx/chat_completed_todos.py b/loopx/chat_completed_todos.py index 99df5e29bf..209c0112a6 100644 --- a/loopx/chat_completed_todos.py +++ b/loopx/chat_completed_todos.py @@ -192,7 +192,7 @@ def _completed_todos(self) -> None: def load(): payload = list_goal_todos( registry_path=self.server.registry_path, goal_id=goal_id, - role="agent", status="done", + role="agent", status="done", read_scope="completed_history", runtime_root_arg=self.server.runtime_root_override, ) ordered = sorted(enumerate(payload["todos"]), key=lambda pair: (str(pair[1].get("completed_at") or ""), pair[0]), reverse=True) diff --git a/loopx/control_plane/todos/goal_todo_projection.py b/loopx/control_plane/todos/goal_todo_projection.py index 8cf94de6fa..1e19923b01 100644 --- a/loopx/control_plane/todos/goal_todo_projection.py +++ b/loopx/control_plane/todos/goal_todo_projection.py @@ -14,7 +14,7 @@ from ..goals.legacy_event_source import require_no_legacy_todo_events from .active_state_editing import TODO_SECTION_HEADINGS -from .active_state_todo_parser import parse_active_state_todos +from .active_state_todo_parser import parse_active_state_todos, parse_todo_source from .list_projection import compact_explicit_limit_todo_summary from .succession_warning import public_todo_summary from .contract import ( @@ -106,6 +106,7 @@ def goal_todo_summaries( todo_id: str | None, agent_id: str | None, limit: int | None, + include_retained: bool = False, ) -> GoalTodoSummaries: """Project todo summaries from active-state text plus its event projection. @@ -115,13 +116,19 @@ def goal_todo_summaries( """ require_no_legacy_todo_events(goal or {}, state_path=state_path) - markdown_fields = parse_active_state_todos( - state_text, - goal=goal, - state_path=state_path, - item_limit=None, - rollout_events=rollout_events, - ) + if include_retained: + active, archived, _sections = parse_todo_source(state_text, goal=goal, state_path=state_path) + markdown_fields = retained_todo_summary_fields( + [*active["user"], *active["agent"], *archived], rollout_events=rollout_events, + ) + else: + markdown_fields = parse_active_state_todos( + state_text, + goal=goal, + state_path=state_path, + item_limit=None, + rollout_events=rollout_events, + ) return todo_summaries_from_fields( fields=markdown_fields, source="markdown_active_state", @@ -182,6 +189,30 @@ def todo_summaries_from_fields( ) +def retained_todo_summary_fields( + items: list[dict[str, Any]], + *, + rollout_events: list[dict[str, Any]], +) -> dict[str, Any]: + """Feed retained source rows through the existing typed summary owner. + + History is an explicit cold read; active scheduler/attention summaries + continue to exclude archives. Preserve full text, evidence and source ids. + """ + return { + f"{role}_todos": compact_todo_group( + [item for item in items if item.get("role") == role], + source_section=TODO_SECTION_HEADINGS[role], + role=role, + include_empty_source=True, + resume_source_items=items, + rollout_events=rollout_events, + item_limit=None, + ) + for role in ("user", "agent") + } + + def exact_archived_todo_summaries( *, archived_items: list[dict[str, Any]], diff --git a/loopx/todos.py b/loopx/todos.py index b015a2015e..97274ea8ec 100644 --- a/loopx/todos.py +++ b/loopx/todos.py @@ -4,7 +4,7 @@ from contextlib import ExitStack from json import dumps as json_dumps from pathlib import Path -from typing import Any +from typing import Any, Literal from .agent_registry import registered_agent_ids_from_registry, require_registered_agent_id from .history import load_registry @@ -84,6 +84,7 @@ ) from .control_plane.todos.goal_todo_projection import ( exact_archived_todo_summaries, + retained_todo_summary_fields, goal_todo_summaries, todo_summaries_from_fields, ) @@ -180,7 +181,12 @@ def list_goal_todos( runtime_root_arg: str | None = None, limit: int | None = None, thin: bool = False, + read_scope: Literal["active", "completed_history"] = "active", ) -> dict[str, Any]: + if read_scope not in {"active", "completed_history"}: + raise ValueError("Todo read_scope must be active or completed_history") + if read_scope == "completed_history" and (role != "agent" or status != "done"): + raise ValueError("Completed history requires role=agent and status=done") normalized_todo_id = normalize_todo_id(todo_id) if todo_id else None if todo_id and not normalized_todo_id: raise ValueError("todo_id must use the public token shape todo_") @@ -216,12 +222,14 @@ def list_goal_todos( ) if canonical_read is not None: projected = todo_summaries_from_fields( - fields=canonical_todo_summary_fields( + fields=(retained_todo_summary_fields( + canonical_todo_items(canonical_read["todos"]), rollout_events=rollout_events, + ) if read_scope == "completed_history" else canonical_todo_summary_fields( canonical_read["todos"], rollout_events=rollout_events, goal_acceptance_contract=canonical_read.get("goal_acceptance_contract"), goal_acceptance_work_guards=canonical_read.get("goal_acceptance_work_guards"), - ), + )), source="file_authority", rollout_events=rollout_events, roles=roles, @@ -244,8 +252,9 @@ def list_goal_todos( todo_id=normalized_todo_id, agent_id=normalized_agent_id, limit=limit, + include_retained=read_scope == "completed_history", ) - if normalized_todo_id and not projected.todos: + if read_scope == "active" and normalized_todo_id and not projected.todos: if canonical_read is not None: archived_items = [ item @@ -278,7 +287,7 @@ def list_goal_todos( matched_todo_count = len(todos) agent_lane_hot_path = bool( - normalized_agent_id and limit is None + read_scope == "active" and normalized_agent_id and limit is None and role is None and status is None and normalized_todo_id is None From fc1d13c512202e8910ef8239fce34e85d4120e6e Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 07:21:30 +0800 Subject: [PATCH 2/7] test: qualify retained completion history across providers Signed-off-by: huangruiteng --- .../project_registry_io_manifest_v1.json | 2 +- tests/test_chat_completed_todos.py | 102 +++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index 9f1f478a29..dd7b47ef19 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -2071,7 +2071,7 @@ }, { "site": "loopx/todos.py::.list_goal_todos::codec_read:load_registry#1", - "line": 192, + "line": 198, "column": 16, "kind": "codec_read", "api": "load_registry", diff --git a/tests/test_chat_completed_todos.py b/tests/test_chat_completed_todos.py index 6a1471afec..b1d3051677 100644 --- a/tests/test_chat_completed_todos.py +++ b/tests/test_chat_completed_todos.py @@ -97,13 +97,27 @@ def read(**kwargs): @pytest.mark.parametrize("count", [85, 4087]) -def test_http_history_reads_real_markdown_without_writes(tmp_path, count): +@pytest.mark.parametrize("archived", [False, True]) +def test_http_history_reads_real_markdown_without_writes(tmp_path, count, archived): state = tmp_path / "active.md" text = (f"Inspect {tmp_path}/results.txt " + "complete task description " * 25)[:420] evidence = f"Verified output at {tmp_path}/results.txt" state.write_text("# Synthetic Goal\n\n## Agent Todo\n" + "\n".join( f"- [x] {text}{index}\n " for index in range(count) ) + "\n\n## User Todo\n", encoding="utf-8") + if archived: + from loopx.control_plane.todos.completed_archive import archive_completed_todo_lines + if count == 85: + result = archive_completed_todo_lines(state.read_text().splitlines(), max_active_done=1) + assert result["moved_count"] == count - 1 + state.write_text("\n".join(result["lines"]) + "\n", encoding="utf-8") + else: + # A pre-existing large archive tests history reads independently of + # the archive writer's per-request batch budget. + lines = state.read_text().splitlines() + first = next(index for index, line in enumerate(lines) if line.startswith("- [x]")) + lines.insert(first + 2, "\n## Completed Work Archive\n") + state.write_text("\n".join(lines).replace("status=done ", "role=agent status=done ")) registry = tmp_path / "registry.json" registry.write_text(json.dumps({"runtime_root": str(tmp_path / "runtime"), "goals": [{"id": "history-goal", "repo": str(tmp_path), "state_file": "active.md"}]})) before = state.read_bytes() @@ -126,8 +140,94 @@ def test_http_history_reads_real_markdown_without_writes(tmp_path, count): with pytest.raises(HTTPError) as denied: urlopen(Request(url, headers={"Origin": "https://unrelated.example"})) assert denied.value.code == 403 + ids = [row["todo_id"] for row in page["items"]] + from urllib.parse import quote + while page["next_cursor"]: + with urlopen(url + "&cursor=" + quote(page["next_cursor"])) as response: + page = json.load(response) + ids.extend(row["todo_id"] for row in page["items"]) + assert len(ids) == len(set(ids)) == count + from loopx.todos import list_goal_todos + active = list_goal_todos(registry_path=registry, goal_id="history-goal", role="agent", status="done") + assert active["todo_count"] == (1 if archived else count) assert state.read_bytes() == before finally: server.shutdown() server.server_close() worker.join() + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_canonical_history_includes_archives_without_widening_active_lists(tmp_path, monkeypatch, provider): + from pathlib import Path + monkeypatch.syspath_prepend(str(Path(__file__).parent / "control_plane")) + from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.todos import list_goal_todos + + isolate_sqlite_runtime(tmp_path, monkeypatch) + state = tmp_path / "state.md" + state.write_text("# Synthetic Goal\n\n## Agent Todo\n\n## Completed Work Archive\n") + runtime = tmp_path / "runtime" + records = [ + {"schema_version": "todo_item_v0", "todo_id": f"todo_history_{index}", + "index": index + 1, "role": "agent", "status": "done", "done": True, + "text": f"Completed task {index}", "archive_state": "archive" if index < 84 else "active", + "source_section": "Completed Work Archive" if index < 84 else "Agent Todo", + "task_class": "advancement_task", "claimed_by": "agent-a" if index % 2 else "agent-b", + "priority": "P2", "note": "Retain full evidence " + "verified " * 50, + "completed_at": f"2026-01-{index % 28 + 1:02d}T00:00:00Z"} + for index in range(85) + ] + records.extend([ + {**records[-1], "todo_id": "todo_monitor", "task_class": "continuous_monitor"}, + {**records[-1], "todo_id": "todo_open", "status": "open", "done": False}, + {**records[-1], "todo_id": "todo_deferred", "status": "deferred"}, + {**records[-1], "todo_id": "todo_user", "role": "user", "task_class": "user_action"}, + ]) + projection = build_todo_runtime_shadow_projection(goal_id="history-goal", todos=records, handoff_mode="soft_claim") + initialize_canonical_authority(runtime, "history-goal", projection, state_path=state, provider=provider) + # A canonical history reader must not fall back to a stale display file. + state.unlink() + registry = tmp_path / "registry.json" + registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [ + {"id": "history-goal", "repo": str(tmp_path), "state_file": "state.md"}]})) + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="history-goal") + active = list_goal_todos(registry_path=registry, goal_id="history-goal", role="agent", status="done") + assert {row["todo_id"] for row in active["todos"]} == {"todo_history_84", "todo_monitor"} + exact = list_goal_todos(registry_path=registry, goal_id="history-goal", todo_id="todo_history_0") + assert exact["todos"][0]["archive_state"] == "archive" + with pytest.raises(ValueError, match="requires role=agent and status=done"): + list_goal_todos(registry_path=registry, goal_id="history-goal", read_scope="completed_history") + server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler) + server.registry_path, server.runtime_root_override, server.verbose = registry, runtime, False + worker = threading.Thread(target=server.serve_forever, daemon=True) + worker.start() + try: + from urllib.parse import quote + base = f"http://127.0.0.1:{server.server_port}/api/chat/completed-todos?goal_id=history-goal" + for suffix, expected in [("", 85), ("&agent_id=agent-a", 42)]: + cursor, rows = "", [] + while True: + with urlopen(base + suffix + "&cursor=" + quote(cursor)) as response: + page = json.load(response) + assert page["total"] == expected + assert len(page["items"]) <= 40 + rows.extend(page["items"]) + cursor = page["next_cursor"] + if not cursor: + break + assert len(rows) == len({row["todo_id"] for row in rows}) == expected + assert all(row["evidence"] == records[0]["note"] for row in rows) + assert all(row["priority"] == "P2" for row in rows) + if suffix: + assert all(row["claimed_by"] == "agent-a" for row in rows) + after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="history-goal") + assert after["provider_revision"] == before["provider_revision"] + assert after["todos"] == before["todos"] + assert not state.exists() + finally: + server.shutdown() + server.server_close() + worker.join() From caa2a35325fdf77982f9cb1ec08e48e30b20e982 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 15:29:48 +0800 Subject: [PATCH 3/7] fix: preserve full completed Todo text and render its evidence Signed-off-by: huangruiteng --- .../src/features/personal-workspace/context-drawer.tsx | 5 +++++ loopx/control_plane/todos/goal_todo_projection.py | 1 + loopx/control_plane/todos/todo_summary.py | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx index a0bfc08c54..40a42f45e8 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx @@ -42,6 +42,7 @@ import { localizedGoalState, localizedSessionStatus, useWorkspaceI18n } from "./ import { formatCostUsd, formatDurationMs, formatTokenCount, formatUsageValue } from "./personal-workspace-model"; import { TeamPlanResult } from "./team-plan-result"; import { parseTodoResumeCondition } from "./todo-resume-condition"; +import { MarkdownText } from "./markdown"; function subagentModelRequest(include: boolean, model: string, effort: string) { if (!include) return {}; @@ -702,6 +703,10 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention : null} + {selection.item.evidence ?
+

{t("drawer.evidence")}

+ +
: null} {selection.item.done ?
{t("drawer.taskCompletedTitle")}{t("drawer.taskCompletedNote")}
: null} ) : null} diff --git a/loopx/control_plane/todos/goal_todo_projection.py b/loopx/control_plane/todos/goal_todo_projection.py index 1e19923b01..f498f364c0 100644 --- a/loopx/control_plane/todos/goal_todo_projection.py +++ b/loopx/control_plane/todos/goal_todo_projection.py @@ -208,6 +208,7 @@ def retained_todo_summary_fields( resume_source_items=items, rollout_events=rollout_events, item_limit=None, + text_limit=None, ) for role in ("user", "agent") } diff --git a/loopx/control_plane/todos/todo_summary.py b/loopx/control_plane/todos/todo_summary.py index fd1ad81cd0..eab1d1c0a6 100644 --- a/loopx/control_plane/todos/todo_summary.py +++ b/loopx/control_plane/todos/todo_summary.py @@ -789,6 +789,7 @@ def _structured_todo_group_items( *, source_section: str | None, role: str | None, + text_limit: int | None, ) -> list[dict[str, Any]]: return [ structured_todo_item( @@ -796,6 +797,7 @@ def _structured_todo_group_items( role=role, source_section=source_section, archive_state=todo_archive_state(item), + text_limit=text_limit, ) if isinstance(item, dict) else item @@ -963,6 +965,7 @@ def compact_todo_group( rollout_events: list[dict[str, Any]] | None = None, available_capabilities: Any = None, item_limit: int | None = MAX_STATUS_TODOS_PER_ROLE, + text_limit: int | None = 500, include_task_orchestration_authority: bool = False, vision_runs: list[dict[str, Any]] | None = None, evaluated_at: str | None = None, @@ -973,6 +976,7 @@ def compact_todo_group( items, source_section=source_section, role=role, + text_limit=text_limit, ) _apply_resume_conditions( items, From fcbd05f66d80d5b369bc8c663998a475bf87b36f Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 15:29:48 +0800 Subject: [PATCH 4/7] test: qualify retained completion history through the packaged workspace Signed-off-by: huangruiteng --- .../2026-09-28-retirement-cadence.md | 16 ++++++++++++-- .../2026-09-28-retirement-cadence.zh-CN.md | 11 +++++++++- .../personal-workspace-browser/fixture.mjs | 2 +- .../typed-actions.mjs | 5 +++++ tests/test_chat_completed_todos.py | 21 +++++++++++++++---- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md index f47c50a389..f471774642 100644 --- a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md +++ b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md @@ -325,8 +325,20 @@ The representative request falls from over 2 MiB to about 0.96 MB without raisin budgets. Real File/SQLite CLI tests cover exact counts, distant inferred edges, metadata preservation and unchanged provider state. This is a bounded capacity repair, not unlimited graph capacity, stable latency evidence, D2 qualification -or permission to change the default provider. Full-Goal summary/list/detail -adoption and sustained observation remain separate work. +or permission to change the default provider. The completed-history consumer +now explicitly reads retained active and archived records through the existing +typed summary/filter owner. Previously, archiving 84 of 85 completions made +the workspace history return only one row. Its bounded HTTP pages now include +all 85, preserve full task text/evidence and Agent filtering, and expose only +display fields. Explicit history reads opt out of the existing 500-character +summary limit; the default active Todo list and scheduling input still exclude +archives and retain their display limit. Real File/SQLite and legacy Markdown +reads preserve provider state. The packaged workspace keeps its existing +pagination and opens retained evidence in the read-only Todo inspector using +the shared Markdown renderer, without another navigation step or edit action. +This closes that history-enumeration/detail gap, not whole-Goal summary/list/detail +adoption, sustained observation, D2 or default-provider admission. No persisted +format, writer or PostgreSQL consumer is changed. ### Packaged-source fingerprint cost diff --git a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md index 8969fb5ed2..46528b200f 100644 --- a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md +++ b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md @@ -242,7 +242,16 @@ Todo/租约集合中位数为 File 输入 32.4→27.5 ms、SQLite 输入 32.9 响应不变。代表性复核请求从超过 2 MiB 降到约 0.96 MB,没有提高预算。 真实 File/SQLite CLI 验证精确计数、跨远端记录的推断后继、metadata 保留与 provider 状态不变。这只修复有界容量,不证明任意规模、稳定延迟、D2 验收, -也不授予默认 provider 切换;整 Goal summary/list/detail 消费和持续观察仍待推进。 +也不授予默认 provider 切换。完成历史消费者现在显式读取活跃与归档的保留记录, +复用既有类型化 summary/filter owner。此前 85 条完成记录归档 84 条后,工作区 +历史只返回 1 条;有界 HTTP 分页现在包含全部 85 条,保留完整任务文本、证据和 +Agent 筛选,仅暴露展示字段。显式历史读取不再套用既有的 500 字符摘要上限; +默认活跃 Todo 列表与调度输入仍排除归档,并保留原展示上限。真实 File/SQLite +和旧 Markdown 读取保持 provider 状态不变。打包工作区沿用已有分页,在只读 +Todo 详情中用共享 Markdown 组件呈现保留证据,不增加导航步骤或编辑动作。 +这关闭历史枚举与详情缺口,不代表整 Goal summary/list/detail +消费、持续观察、D2 或默认 provider 准入完成;不改变持久格式、writer 或 +PostgreSQL 消费者。 ### 打包源码指纹成本 diff --git a/examples/personal-workspace-browser/fixture.mjs b/examples/personal-workspace-browser/fixture.mjs index ca1cf8c2c7..02953e90a9 100644 --- a/examples/personal-workspace-browser/fixture.mjs +++ b/examples/personal-workspace-browser/fixture.mjs @@ -870,7 +870,7 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true const offset = Number(url.searchParams.get("cursor") || 0); const items = Array.from({ length: Math.min(40, total - offset) }, (_, position) => { const index = offset + position; - return { todo_id: `todo_history_${index}`, text: index < 3 ? `Completed ${String.fromCharCode(65 + index)}` : `Completed historical Task ${index + 1}`, claimed_by: "example-agent", evidence: null, priority: null, task_class: "advancement_task" }; + return { todo_id: `todo_history_${index}`, text: index < 3 ? `Completed ${String.fromCharCode(65 + index)}` : `Completed historical Task ${index + 1}`, claimed_by: "example-agent", evidence: index === 0 ? "Verified retained **completion evidence**." : null, priority: null, task_class: "advancement_task" }; }); await route.fulfill({ json: { ok: true, total, items, next_cursor: offset + 40 < total ? String(offset + 40) : null } }); return; diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index 1b38dfafb9..590d74585d 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -907,6 +907,11 @@ export const typedActionsScenario = { await page.screenshot({ path: resolve(outputDir, 'completed-history-4087.png'), fullPage: false, animations: 'disabled' }); await historyScroll.evaluate(element => { element.scrollTop = 0; }); await completedColumn.getByText('Completed A', { exact: true }).waitFor(); + await completedColumn.getByText('Completed A', { exact: true }).click(); + const completedDetail = page.getByRole('dialog', { name: 'Todo 详情' }); + await completedDetail.getByRole('region', { name: '证据', exact: true }).getByText('Verified retained completion evidence.', { exact: true }).waitFor(); + if (await completedDetail.getByRole('button', { name: '完成任务', exact: true }).count()) throw new Error('Completed history exposed a write action'); + await page.keyboard.press('Escape'); // Both presentations retain one snapshot, including archived history and evidence. let historyRequests = 0; page.on('request', request => { if (request.url().includes('/api/chat/completed-todos?')) historyRequests += 1; }); diff --git a/tests/test_chat_completed_todos.py b/tests/test_chat_completed_todos.py index b1d3051677..4b3fd789a2 100644 --- a/tests/test_chat_completed_todos.py +++ b/tests/test_chat_completed_todos.py @@ -100,7 +100,7 @@ def read(**kwargs): @pytest.mark.parametrize("archived", [False, True]) def test_http_history_reads_real_markdown_without_writes(tmp_path, count, archived): state = tmp_path / "active.md" - text = (f"Inspect {tmp_path}/results.txt " + "complete task description " * 25)[:420] + text = f"Inspect {tmp_path}/results.txt " + " ".join(["complete task description"] * 25) evidence = f"Verified output at {tmp_path}/results.txt" state.write_text("# Synthetic Goal\n\n## Agent Todo\n" + "\n".join( f"- [x] {text}{index}\n " for index in range(count) @@ -135,17 +135,19 @@ def test_http_history_reads_real_markdown_without_writes(tmp_path, count, archiv assert len(page["items"]) == 40 assert page["next_cursor"] assert page["items"][0]["text"].startswith(text) - assert len(page["items"][0]["text"]) > 400 + assert len(page["items"][0]["text"]) > 600 assert page["items"][0]["evidence"] == evidence with pytest.raises(HTTPError) as denied: urlopen(Request(url, headers={"Origin": "https://unrelated.example"})) assert denied.value.code == 403 ids = [row["todo_id"] for row in page["items"]] + assert all(row["text"] == text + row["todo_id"].removeprefix("todo_history_") for row in page["items"]) from urllib.parse import quote while page["next_cursor"]: with urlopen(url + "&cursor=" + quote(page["next_cursor"])) as response: page = json.load(response) ids.extend(row["todo_id"] for row in page["items"]) + assert all(row["text"] == text + row["todo_id"].removeprefix("todo_history_") for row in page["items"]) assert len(ids) == len(set(ids)) == count from loopx.todos import list_goal_todos active = list_goal_todos(registry_path=registry, goal_id="history-goal", role="agent", status="done") @@ -173,10 +175,12 @@ def test_canonical_history_includes_archives_without_widening_active_lists(tmp_p records = [ {"schema_version": "todo_item_v0", "todo_id": f"todo_history_{index}", "index": index + 1, "role": "agent", "status": "done", "done": True, - "text": f"Completed task {index}", "archive_state": "archive" if index < 84 else "active", + "text": f"Completed task {index}: " + " ".join(["Full retained task description"] * 20), + "archive_state": "archive" if index < 84 else "active", "source_section": "Completed Work Archive" if index < 84 else "Agent Todo", "task_class": "advancement_task", "claimed_by": "agent-a" if index % 2 else "agent-b", - "priority": "P2", "note": "Retain full evidence " + "verified " * 50, + "priority": "P2", "required_capabilities": ["local-fixture-capability"], + "note": "Retain full evidence " + "verified " * 50, "completed_at": f"2026-01-{index % 28 + 1:02d}T00:00:00Z"} for index in range(85) ] @@ -196,10 +200,13 @@ def test_canonical_history_includes_archives_without_widening_active_lists(tmp_p before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="history-goal") active = list_goal_todos(registry_path=registry, goal_id="history-goal", role="agent", status="done") assert {row["todo_id"] for row in active["todos"]} == {"todo_history_84", "todo_monitor"} + assert all(len(row["text"]) <= 500 and row["text"].endswith("…") for row in active["todos"]) exact = list_goal_todos(registry_path=registry, goal_id="history-goal", todo_id="todo_history_0") assert exact["todos"][0]["archive_state"] == "archive" with pytest.raises(ValueError, match="requires role=agent and status=done"): list_goal_todos(registry_path=registry, goal_id="history-goal", read_scope="completed_history") + with pytest.raises(ValueError, match="read_scope must be"): + list_goal_todos(registry_path=registry, goal_id="history-goal", read_scope="unknown") server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler) server.registry_path, server.runtime_root_override, server.verbose = registry, runtime, False worker = threading.Thread(target=server.serve_forever, daemon=True) @@ -221,6 +228,12 @@ def test_canonical_history_includes_archives_without_widening_active_lists(tmp_p assert len(rows) == len({row["todo_id"] for row in rows}) == expected assert all(row["evidence"] == records[0]["note"] for row in rows) assert all(row["priority"] == "P2" for row in rows) + source_by_id = {record["todo_id"]: record for record in records} + assert all(row["text"] == source_by_id[row["todo_id"]]["text"] for row in rows) + # Workspace display retains complete text/evidence, not the + # authority's internal capability or lifecycle metadata. + assert all(set(row) == {"todo_id", "text", "claimed_by", "evidence", "priority", "task_class"} + for row in rows) if suffix: assert all(row["claimed_by"] == "agent-a" for row in rows) after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="history-goal") From fded5a8173ea56a6b933c6e3c10e217b84377e7d Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 16:33:41 +0800 Subject: [PATCH 5/7] fix(workspace): preserve retained task inspector metadata Signed-off-by: huangruiteng --- .../completed-task-lane.tsx | 13 +++- .../personal-workspace/context-drawer.tsx | 7 +- .../src/features/personal-workspace/i18n.tsx | 2 + .../personal-workspace-model.ts | 31 +++++++++ .../dashboard/src/views/dashboard-page.tsx | 68 +++++-------------- loopx/chat_completed_todos.py | 10 +++ 6 files changed, 73 insertions(+), 58 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/completed-task-lane.tsx b/apps/presentation/dashboard/src/features/personal-workspace/completed-task-lane.tsx index 2c1643b455..5cc434414a 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/completed-task-lane.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/completed-task-lane.tsx @@ -1,12 +1,19 @@ import { useEffect, useEffectEvent, useId, useRef, useState } from "react"; import { z } from "zod"; -import {compactWorkspaceText} from "./personal-workspace-model"; +import {compactWorkspaceText, workspaceAgentTodoFromItem} from "./personal-workspace-model"; +import { todoItemSchema } from "../../data/status"; import type { WorkspaceAgentTodo, WorkspaceDrawerSelection, WorkspaceGoal } from "./personal-workspace-model"; import { useWorkspaceI18n } from "./i18n"; const pageSchema = z.object({ ok: z.literal(true), total: z.number().int().nonnegative(), next_cursor: z.string().nullable(), - items: z.array(z.object({ todo_id: z.string(), text: z.string(), claimed_by: z.string().nullable(), evidence: z.string().nullable(), priority: z.string().nullable(), task_class: z.string().nullable() })).max(40), + items: z.array(z.object(todoItemSchema.shape).pick({ + todo_id: true, text: true, done: true, status: true, claimed_by: true, + evidence: true, note: true, priority: true, task_class: true, task_domain: true, + completed_at: true, resume_when: true, resume_ready: true, resume_condition: true, + completion_validation_sha256: true, completion_validation_revision: true, + completion_validation_revision_history: true, + }).extend({ todo_id: z.string().min(1), done: z.literal(true), status: z.literal("done") })).max(40), }); /** Fixed-height previews keep layout/DOM cost bounded; the drawer retains full text. */ @@ -69,7 +76,7 @@ export function CompletedTaskLane({ goal, agentId, seed, enabled, refreshRevisio if (!response.ok) throw new Error("history unavailable"); const page = pageSchema.parse(await response.json()); if (controller.signal.aborted) return; - const next = page.items.map((todo): WorkspaceAgentTodo => ({ todoId: todo.todo_id, text: todo.text, claimedBy: todo.claimed_by, evidence: todo.evidence, priority: todo.priority, taskClass: todo.task_class, done: true, status: "done" })); + const next = page.items.map((todo) => workspaceAgentTodoFromItem(todo, todo.todo_id)); setRows((current) => cursor === undefined ? next : [...current, ...next.filter((todo) => !current.some((existing) => existing.todoId === todo.todoId))]); setTotal(page.total); setCursor(page.next_cursor); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx index 40a42f45e8..82396fbb0e 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx @@ -638,12 +638,13 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
{t("common.owner")}
{selection.item.ownerLabel ?? selection.item.claimedBy ?? t("drawer.notAssigned")}
{t("common.status")}
{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}
{t("drawer.priority")}
{selection.item.priority ?? t("drawer.notSet")}
-
{t("drawer.dependencies")}
{selection.item.dependencies?.join(" · ") || t("common.none")}
+ {selection.item.completedAt ?
{t("drawer.completedAt")}
: null} + {selection.item.dependencies ?
{t("drawer.dependencies")}
{selection.item.dependencies.join(" · ") || t("common.none")}
: null} {selection.item.status === "deferred" || selection.item.resumeWhen ?
{t("drawer.resumeWhen")}
{selection.item.resumeWhen || t("drawer.notSet")}
: null} - {selection.item.resumeWhen ?
{t("drawer.resumeState")}
{selection.item.resumeReady ? t("drawer.resumeReady") : t("drawer.resumePending")}
: null} + {selection.item.resumeWhen ?
{t("drawer.resumeState")}
{selection.item.resumeReady == null ? t("drawer.notSet") : selection.item.resumeReady ? t("drawer.resumeReady") : t("drawer.resumePending")}
: null} {selection.item.resumeReceiptId ?
{t("drawer.resumeReceipt")}
{selection.item.resumeReceiptId}
: null} {selection.item.validationDigest ? <> -
{t("drawer.validationRevision")}
{selection.item.validationRevision ?? 0}
+
{t("drawer.validationRevision")}
{selection.item.validationRevision ?? t("drawer.notSet")}
{t("drawer.validationDigest")}
{selection.item.validationDigest}
{selection.item.validationRevisionActor ?
{t("drawer.validationRevisionActor")}
{selection.item.validationRevisionActor}
: null} : null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx index 7af8eb78bb..67ae3bd765 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx @@ -216,6 +216,7 @@ const en = { "drawer.nextTransition": "Next transition", "drawer.validationRevision": "Validation revision", "drawer.validationDigest": "Validation declaration digest", + "drawer.completedAt": "Completed at", "drawer.validationRevisionActor": "Revised by", "drawer.noExecutionHistory": "No execution history yet.", "drawer.noRun": "Execution Session has not started", @@ -1414,6 +1415,7 @@ const zhCN: Record = { "drawer.nextTransition": "下一转换", "drawer.validationRevision": "验证声明修订", "drawer.validationDigest": "验证声明摘要", + "drawer.completedAt": "完成时间", "drawer.validationRevisionActor": "修订者", "drawer.noExecutionHistory": "暂无执行记录。", "drawer.noRun": "尚未启动执行 Session", diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts index 64e9966242..a1df6c1a0a 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts @@ -5,6 +5,7 @@ import type { ActionReviewPlan } from "../../../../../../loopx/control_plane/pre import type { GoalAcceptanceObservation } from "../../data/goal-acceptance-observation"; import type { AttentionDetails } from "./attention-details"; import type { WorkspaceLoadError, WorkspaceReadScope } from "../../data/workspace-progressive-status"; +import type { TodoItem } from "../../data/status"; import { goalWorkKind, type GoalHostThreadActivity, type WorkspaceGoalExecution } from "./goal-activity"; export type WorkspaceGoalState = | "需修复" @@ -45,6 +46,36 @@ export type WorkspaceAgentTodo = { validationRevisionActor?: string | null; }; +/** Both active status and retained history carry the same inspector facts. */ +export function workspaceAgentTodoFromItem(todo: Pick, fallbackId: string): WorkspaceAgentTodo { + const receipt = todo.resume_condition?.resume_receipt; + const receiptId = receipt && typeof receipt === "object" && !Array.isArray(receipt) + ? (receipt as Record).receipt_id : null; + return { + todoId: todo.todo_id?.trim() || fallbackId, + text: todo.text, + done: todo.status === "deferred" ? false : todo.done, + status: todo.status ?? null, + claimedBy: todo.claimed_by ?? null, + evidence: todo.evidence || todo.note || null, + priority: todo.priority ?? null, + taskClass: todo.task_class ?? null, + taskDomain: todo.task_domain ?? null, + completedAt: todo.completed_at ?? null, + resumeWhen: todo.resume_when ?? null, + resumeReady: todo.resume_ready ?? null, + resumeReceiptId: typeof receiptId === "string" && receiptId.trim() ? receiptId.trim() : null, + validationDigest: todo.completion_validation_sha256 ?? null, + validationRevision: todo.completion_validation_revision ?? null, + validationRevisionActor: todo.completion_validation_revision_history.at(-1)?.actor_agent_id ?? null, + }; +} + export type WorkspaceTodo = WorkspaceAgentTodo & { goalId: string; goalTitle: string; diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index fb41de5ba3..703ff92240 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -1,7 +1,7 @@ import { normalizeGoalDraft, type GoalDraft } from "../../../../../loopx/control_plane/collaboration/goal_draft.js"; import { conversationReturnSessions, reconcileConversationHistory, reconcileConversationReturns } from "../data/conversation-returns"; import { currentChannelSession, useConversationHistory } from "../data/use-conversation-history"; -import {compactWorkspaceText as compactShareText} from "../features/personal-workspace/personal-workspace-model"; +import {compactWorkspaceText as compactShareText, workspaceAgentTodoFromItem} from "../features/personal-workspace/personal-workspace-model"; import type { GoalAcceptanceObservation } from "../data/goal-acceptance-observation"; import { attentionDetails, sourceAttention } from "../features/personal-workspace/attention-details"; import type { AttentionDetails } from "../features/personal-workspace/attention-details"; @@ -98,6 +98,7 @@ import { import { goalHasExecutionSummary, normalizePersonalHomeModel, + type WorkspaceAgentTodo, type WorkspaceAgentOption, type WorkspaceAttention, type WorkspaceGoal, @@ -217,24 +218,7 @@ type TodoExplorerItem = { todo: TodoItem; }; -type PersonalAgentTodoItem = { - completedAt?: string | null; - resumeWhen?: string | null; - resumeReady?: boolean | null; - resumeReceiptId?: string | null; - claimedBy?: string | null; - done: boolean; - evidence?: string | null; - priority?: string | null; - status?: string | null; - taskClass?: string | null; - taskDomain?: string | null; - text: string; - todoId: string; - validationDigest?: string | null; - validationRevision?: number | null; - validationRevisionActor?: string | null; -}; + function inferLifecyclePhase(status?: string | null, run?: RunRecord) { if (run?.controller_readiness?.decision_advisor_ready || run?.controller_readiness?.write_controller_ready) { @@ -453,7 +437,7 @@ type PersonalGoalItem = { activationState: "active" | "stopped"; agentId: string; agentSentence: string; - agentTodos: PersonalAgentTodoItem[]; + agentTodos: WorkspaceAgentTodo[]; doneTodoCount: number; goalId: string; latestActivity?: string; @@ -717,33 +701,13 @@ function personalTodoText(todo: TodoItem) { return compactShareText(todo.title ?? todo.text, 112); } -function personalTodoResumeReceiptId(todo: TodoItem) { - const receipt = todo.resume_condition?.resume_receipt; - if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return null; - const receiptId = (receipt as Record).receipt_id; - return typeof receiptId === "string" && receiptId.trim() ? receiptId.trim() : null; -} - -function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): PersonalAgentTodoItem { - const latestValidationRevision = todo.completion_validation_revision_history.at(-1); +function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): WorkspaceAgentTodo { return { - completedAt: todo.completed_at ?? null, - resumeWhen: todo.resume_when ?? null, - resumeReady: todo.resume_ready ?? null, - resumeReceiptId: personalTodoResumeReceiptId(todo), - claimedBy: todo.claimed_by ?? null, - // Legacy summaries mark deferred entries checked; they are not completed work. - done: todo.status === "deferred" ? false : todo.done, + ...workspaceAgentTodoFromItem(todo, `${row.goal.id}:agent:${todo.index}`), + // Preserve the active queue's existing preview budget. History and its + // inspector use full text/evidence from the retained read. evidence: todo.evidence ? compactShareText(todo.evidence, 96) : null, - priority: todo.priority ?? null, - status: todo.status ?? null, - taskClass: todo.task_class ?? null, - taskDomain: todo.task_domain ?? null, text: personalTodoText(todo), - todoId: todo.todo_id?.trim() || `${row.goal.id}:agent:${todo.index}`, - validationDigest: todo.completion_validation_sha256 ?? null, - validationRevision: todo.completion_validation_revision ?? null, - validationRevisionActor: latestValidationRevision?.actor_agent_id ?? null, }; } @@ -760,16 +724,16 @@ function personalAgentTodoItems(row: GoalDirectoryRow): TodoItem[] { return [...merged.values()]; } -function personalAgentTodos(row: GoalDirectoryRow): PersonalAgentTodoItem[] { +function personalAgentTodos(row: GoalDirectoryRow): WorkspaceAgentTodo[] { return personalAgentTodoItems(row).map((todo) => personalAgentTodoFromItem(todo, row)); } function personalSubagentDomainCandidates( payload: StatusPayload, row: GoalDirectoryRow, - fallbackTodos: PersonalAgentTodoItem[], + fallbackTodos: WorkspaceAgentTodo[], ) { - const candidateTodos = new Map(); + const candidateTodos = new Map(); for (const todo of payload.todo_index?.items ?? []) { if (todo.goal_id !== row.goal.id || todo.role !== "agent") continue; const projected = personalAgentTodoFromItem(todo, row); @@ -792,7 +756,7 @@ function personalSubagentDomainCandidates( function personalAgentTodoFromProjection( todo: NonNullable, row: GoalDirectoryRow, -): PersonalAgentTodoItem { +): WorkspaceAgentTodo { return { claimedBy: todo.claimed_by ?? null, done: todo.status === "done" || todo.status === "completed", @@ -805,10 +769,10 @@ function personalAgentTodoFromProjection( } function mergePersonalAgentTodos( - projectedTodos: PersonalAgentTodoItem[], + projectedTodos: WorkspaceAgentTodo[], agentRows: AgentManagementRow[], row: GoalDirectoryRow, -): PersonalAgentTodoItem[] { +): WorkspaceAgentTodo[] { const merged = new Map(projectedTodos.map((todo) => [todo.todoId, todo])); for (const agent of agentRows) { const current = agent.currentTodo; @@ -827,7 +791,7 @@ function mergePersonalAgentTodos( function personalAgentTodoFacts(row: GoalDirectoryRow): { doneTodoCount: number; nextTodoText: string | null; - recentCompleted: PersonalAgentTodoItem[]; + recentCompleted: WorkspaceAgentTodo[]; } { const assetTodos = row.queueItem?.project_asset?.agent_todos; const queueTodos = row.queueItem?.agent_todos; @@ -854,7 +818,7 @@ function personalAgentTodoFacts(row: GoalDirectoryRow): { return { doneTodoCount, nextTodoText, recentCompleted }; } -function personalVisiblePlanTodos(todos: PersonalAgentTodoItem[], limit = 4) { +function personalVisiblePlanTodos(todos: WorkspaceAgentTodo[], limit = 4) { if (todos.length <= limit) { return todos; } diff --git a/loopx/chat_completed_todos.py b/loopx/chat_completed_todos.py index 209c0112a6..59b92eda61 100644 --- a/loopx/chat_completed_todos.py +++ b/loopx/chat_completed_todos.py @@ -204,6 +204,16 @@ def load(): "evidence": item.get("evidence") or item.get("note") or None, "priority": item.get("priority"), "task_class": item.get("task_class"), + "done": item["done"], + "status": item["status"], + "task_domain": item.get("task_domain"), + "completed_at": item.get("completed_at"), + "resume_when": item.get("resume_when"), + "resume_ready": item.get("resume_ready"), + "resume_condition": item.get("resume_condition"), + "completion_validation_sha256": item.get("completion_validation_sha256"), + "completion_validation_revision": item.get("completion_validation_revision"), + "completion_validation_revision_history": item.get("completion_validation_revision_history") or [], } for _, item in ordered if item.get("todo_id") and item.get("task_class") != "continuous_monitor" From 8ff70ad0439797860e7d5a07d7becc23b396bf66 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 16:33:41 +0800 Subject: [PATCH 6/7] test(workspace): qualify retained history inspector facts Signed-off-by: huangruiteng --- .../2026-09-28-retirement-cadence.md | 11 +++++++- .../2026-09-28-retirement-cadence.zh-CN.md | 9 +++++- .../personal-workspace-browser/fixture.mjs | 8 +++++- .../typed-actions.mjs | 6 ++++ ...pace-progressive-loading-browser-smoke.mjs | 2 +- tests/test_chat_completed_todos.py | 28 ++++++++++++++++--- 6 files changed, 56 insertions(+), 8 deletions(-) diff --git a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md index f471774642..289076df8d 100644 --- a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md +++ b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.md @@ -325,7 +325,11 @@ The representative request falls from over 2 MiB to about 0.96 MB without raisin budgets. Real File/SQLite CLI tests cover exact counts, distant inferred edges, metadata preservation and unchanged provider state. This is a bounded capacity repair, not unlimited graph capacity, stable latency evidence, D2 qualification -or permission to change the default provider. The completed-history consumer +or permission to change the default provider. + +### Retained completion consumption + +The completed-history consumer now explicitly reads retained active and archived records through the existing typed summary/filter owner. Previously, archiving 84 of 85 completions made the workspace history return only one row. Its bounded HTTP pages now include @@ -336,6 +340,11 @@ archives and retain their display limit. Real File/SQLite and legacy Markdown reads preserve provider state. The packaged workspace keeps its existing pagination and opens retained evidence in the read-only Todo inspector using the shared Markdown renderer, without another navigation step or edit action. +History also preserves completion time, resume facts and validation-declaration +revision/actor facts. Active and history consumers reuse the same TS inspector +mapping and the existing Todo schema; the active queue's preview budgets remain +unchanged. Unprovided dependencies are not presented as an empty dependency +list, and absent resume readiness or validation revisions are not invented. This closes that history-enumeration/detail gap, not whole-Goal summary/list/detail adoption, sustained observation, D2 or default-provider admission. No persisted format, writer or PostgreSQL consumer is changed. diff --git a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md index 46528b200f..199e112711 100644 --- a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md +++ b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-28-retirement-cadence.zh-CN.md @@ -242,13 +242,20 @@ Todo/租约集合中位数为 File 输入 32.4→27.5 ms、SQLite 输入 32.9 响应不变。代表性复核请求从超过 2 MiB 降到约 0.96 MB,没有提高预算。 真实 File/SQLite CLI 验证精确计数、跨远端记录的推断后继、metadata 保留与 provider 状态不变。这只修复有界容量,不证明任意规模、稳定延迟、D2 验收, -也不授予默认 provider 切换。完成历史消费者现在显式读取活跃与归档的保留记录, +也不授予默认 provider 切换。 + +### 保留完成记录的消费 + +完成历史消费者现在显式读取活跃与归档的保留记录, 复用既有类型化 summary/filter owner。此前 85 条完成记录归档 84 条后,工作区 历史只返回 1 条;有界 HTTP 分页现在包含全部 85 条,保留完整任务文本、证据和 Agent 筛选,仅暴露展示字段。显式历史读取不再套用既有的 500 字符摘要上限; 默认活跃 Todo 列表与调度输入仍排除归档,并保留原展示上限。真实 File/SQLite 和旧 Markdown 读取保持 provider 状态不变。打包工作区沿用已有分页,在只读 Todo 详情中用共享 Markdown 组件呈现保留证据,不增加导航步骤或编辑动作。 +历史同时保留完成时间、恢复事实及验收声明的版本/修订者。活跃列表和历史 +复用同一 TS 详情映射与既有 Todo schema,活跃队列的预览预算保持不变。 +未提供的依赖列表不再展示为“无依赖”,缺失的恢复就绪性或验收版本也不虚构。 这关闭历史枚举与详情缺口,不代表整 Goal summary/list/detail 消费、持续观察、D2 或默认 provider 准入完成;不改变持久格式、writer 或 PostgreSQL 消费者。 diff --git a/examples/personal-workspace-browser/fixture.mjs b/examples/personal-workspace-browser/fixture.mjs index 02953e90a9..ee75c6c01e 100644 --- a/examples/personal-workspace-browser/fixture.mjs +++ b/examples/personal-workspace-browser/fixture.mjs @@ -870,7 +870,13 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true const offset = Number(url.searchParams.get("cursor") || 0); const items = Array.from({ length: Math.min(40, total - offset) }, (_, position) => { const index = offset + position; - return { todo_id: `todo_history_${index}`, text: index < 3 ? `Completed ${String.fromCharCode(65 + index)}` : `Completed historical Task ${index + 1}`, claimed_by: "example-agent", evidence: index === 0 ? "Verified retained **completion evidence**." : null, priority: null, task_class: "advancement_task" }; + return { todo_id: `todo_history_${index}`, text: index < 3 ? `Completed ${String.fromCharCode(65 + index)}` : `Completed historical Task ${index + 1}`, claimed_by: "example-agent", evidence: index === 0 ? "Verified retained **completion evidence**." : null, priority: null, task_class: "advancement_task", done: true, status: "done", + completed_at: "2026-08-01T00:00:00Z", resume_when: index === 0 ? "todo_done:todo_history_1" : null, resume_ready: index === 0 ? true : null, + resume_condition: index === 0 ? { resume_receipt: { receipt_id: "receipt-completed-history" } } : null, + completion_validation_sha256: index === 0 ? "a".repeat(64) : null, + completion_validation_revision: index === 0 ? 3 : null, + completion_validation_revision_history: index === 0 ? [{ revision: 3, previous_declaration_sha256: "b".repeat(64), declaration_sha256: "a".repeat(64), actor_agent_id: "example-reviewer", revised_at: "2026-08-01T00:00:00Z" }] : [], + }; }); await route.fulfill({ json: { ok: true, total, items, next_cursor: offset + 40 < total ? String(offset + 40) : null } }); return; diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index 590d74585d..0a313d49cf 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -910,6 +910,12 @@ export const typedActionsScenario = { await completedColumn.getByText('Completed A', { exact: true }).click(); const completedDetail = page.getByRole('dialog', { name: 'Todo 详情' }); await completedDetail.getByRole('region', { name: '证据', exact: true }).getByText('Verified retained completion evidence.', { exact: true }).waitFor(); + await completedDetail.getByText('todo_done:todo_history_1', { exact: true }).waitFor(); + await completedDetail.getByText('receipt-completed-history', { exact: true }).waitFor(); + await completedDetail.getByText('example-reviewer', { exact: true }).waitFor(); + await completedDetail.getByText('a'.repeat(64), { exact: true }).waitFor(); + await completedDetail.getByText('2026-08-01T00:00:00Z', { exact: true }).waitFor(); + if (await completedDetail.getByText('依赖', { exact: true }).count()) throw new Error('An absent dependency projection was reported as no dependencies'); if (await completedDetail.getByRole('button', { name: '完成任务', exact: true }).count()) throw new Error('Completed history exposed a write action'); await page.keyboard.press('Escape'); // Both presentations retain one snapshot, including archived history and evidence. diff --git a/examples/workspace-progressive-loading-browser-smoke.mjs b/examples/workspace-progressive-loading-browser-smoke.mjs index f814ed3310..8c6bcead61 100644 --- a/examples/workspace-progressive-loading-browser-smoke.mjs +++ b/examples/workspace-progressive-loading-browser-smoke.mjs @@ -138,7 +138,7 @@ try { historyRequests++; return route.fulfill({ json: { ok: true, total: 1, next_cursor: null, items: [{ todo_id: "todo_native_done", text: historyText, claimed_by: null, - evidence: "Verified public evidence", priority: "P2", task_class: "advancement_task", + evidence: "Verified public evidence", priority: "P2", task_class: "advancement_task", done: true, status: "done", }] } }); }); await accessPage.goto(`${origin}/chat/`); diff --git a/tests/test_chat_completed_todos.py b/tests/test_chat_completed_todos.py index 4b3fd789a2..c3de1cc5dd 100644 --- a/tests/test_chat_completed_todos.py +++ b/tests/test_chat_completed_todos.py @@ -184,6 +184,17 @@ def test_canonical_history_includes_archives_without_widening_active_lists(tmp_p "completed_at": f"2026-01-{index % 28 + 1:02d}T00:00:00Z"} for index in range(85) ] + records[0].update({ + "task_domain": "quality", + "resume_when": "todo_done:todo_history_84", + "completion_validation_sha256": "a" * 64, + "completion_validation_revision": 3, + "completion_validation_revision_history": [{ + "revision": 3, "previous_declaration_sha256": "b" * 64, + "declaration_sha256": "a" * 64, "actor_agent_id": "agent-a", + "revised_at": "2026-01-01T00:00:00Z", + }], + }) records.extend([ {**records[-1], "todo_id": "todo_monitor", "task_class": "continuous_monitor"}, {**records[-1], "todo_id": "todo_open", "status": "open", "done": False}, @@ -230,10 +241,19 @@ def test_canonical_history_includes_archives_without_widening_active_lists(tmp_p assert all(row["priority"] == "P2" for row in rows) source_by_id = {record["todo_id"]: record for record in records} assert all(row["text"] == source_by_id[row["todo_id"]]["text"] for row in rows) - # Workspace display retains complete text/evidence, not the - # authority's internal capability or lifecycle metadata. - assert all(set(row) == {"todo_id", "text", "claimed_by", "evidence", "priority", "task_class"} - for row in rows) + assert all(row["completed_at"] == source_by_id[row["todo_id"]]["completed_at"] for row in rows) + assert all(row["done"] is True and row["status"] == "done" for row in rows) + # Existing inspector facts travel with their source, while private + # execution declarations and capability metadata remain excluded. + assert all("required_capabilities" not in row and "completion_validation" not in row for row in rows) + if not suffix: + detail = next(row for row in rows if row["todo_id"] == "todo_history_0") + assert detail["task_domain"] == "quality" + assert detail["resume_when"] == "todo_done:todo_history_84" + assert detail["resume_ready"] is True + assert detail["completion_validation_sha256"] == "a" * 64 + assert detail["completion_validation_revision"] == 3 + assert detail["completion_validation_revision_history"] == records[0]["completion_validation_revision_history"] if suffix: assert all(row["claimed_by"] == "agent-a" for row in rows) after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="history-goal") From 6e2d0608f6f70bfcfb4508c51e32c2cf47d024e8 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Sat, 3 Oct 2026 14:10:57 +0800 Subject: [PATCH 7/7] fix: reuse existing workspace Todo type import Signed-off-by: huangruiteng --- apps/presentation/dashboard/src/views/dashboard-page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index 98fb6033fe..ca3d489a3c 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -98,7 +98,6 @@ import { import { goalHasExecutionSummary, normalizePersonalHomeModel, - type WorkspaceAgentTodo, type WorkspaceAgentOption, type WorkspaceAttention, type WorkspaceGoal,