From ffa9cd78baa54492adef4ed4e9fd939f9ae4e5ab Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:07:39 +0800 Subject: [PATCH 1/3] refactor(context): read selected Todo sources inside TypeScript Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- ...2026-11-04-selected-context-source-read.md | 22 +++++ ...1-04-selected-context-source-read.zh-CN.md | 22 +++++ .../typescript-control-plane-migration-v0.md | 1 + ...script-control-plane-migration-v0.zh-CN.md | 1 + .../control_plane/effect_runtime_handlers.ts | 2 +- loopx/control_plane/todos/read_codec.ts | 6 +- .../work_items/context_readback.py | 42 ++-------- .../work_items/interaction_contract.ts | 50 ++++++++++++ .../test_selected_work_requirements.py | 39 ++++----- .../interaction_context_readback.test.ts | 80 +++++++++++++++++++ 10 files changed, 205 insertions(+), 60 deletions(-) create mode 100644 docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md create mode 100644 docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md create mode 100644 tests/control_plane_ts/interaction_context_readback.test.ts diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md new file mode 100644 index 0000000000..c0326a400f --- /dev/null +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md @@ -0,0 +1,22 @@ +# Selected Todo source read in the existing context transaction + +The accepted request is to retire Python bridge work without creating parallel semantic owners. This slice moves the complete selected-Todo source read and its source-fact construction into the existing `work_item.context.project` request. Goal document, User-scope and acceptance/oracle IO remain Python providers. + +| Boundary | Result | +| --- | --- | +| Before | Python reads the complete canonical collection, selects one active Todo, adds its content revision, then sends it to the TS context projector. | +| After | `readAndProjectInteractionWorkContext` uses the existing canonical snapshot reader in-process, then invokes the unchanged context validator/projector. | +| Retirement | The Python selected-Todo branch and source-reader imports are removed. The containing adapter remains for its real Goal/acceptance IO callers. | +| Integrity | The same existing-only store route, complete snapshot validation, page/revision fencing, archived/missing refusal, raw body, normalized content revision, status and claim checks remain. There is no Markdown fallback. | +| Transport | No RPC method is added. The existing context projection receives `source_context`; the canonical read retains its existing 15-second budget. Source delivery still uses two coarse context requests around the remaining Python providers. | +| Feature off | A request without a selected-Todo required read does not open a provider. Prepared-source native callers retain the existing pure projection. | +| Recovery | A changed selected body fails the public CLI on both File and SQLite. Bypassing the typed source guard makes both independent regression cases fail. | +| Authority | This read grants no claim, lease, execution, acceptance or write authority. No persisted format or live Goal data is migrated. | + +## Evidence and limits + +The immutable initial baseline is `ecbf905402ccfd9e4d75b2b963b52376b4ef7332`; later-main integration and final exact-head evidence belong in the PR. Existing failures in four selected-work tests reproduce on that baseline and concern the prior capture/output layout. Their repair belongs to existing PR #7430; this slice does not relax those assertions. + +Durable checks live in `tests/control_plane_ts/interaction_context_readback.test.ts` and `tests/control_plane/test_selected_work_requirements.py`. They cover complete requirements/notes, source revision, changed/archived/missing/corrupt/unavailable state, no provider creation, feature-off behavior and unchanged canonical data. Wheel and sdist execution, matched full-CLI costs and actual source revision are recorded separately in the PR; test counts or fewer requests do not prove whole-Goal outcome value. + +The shared normalized-text revision helper serves the existing canonical display codec and the new source read. No policy is reimplemented in Python. Retire the remaining Python context adapter only when its document, User-scope and oracle providers have real native callers; removing a file based on its language is not the exit criterion. diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md new file mode 100644 index 0000000000..c8e903dc9c --- /dev/null +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md @@ -0,0 +1,22 @@ +# 当前 Todo 的来源读取并入现有上下文事务 + +本轮要求是退役 Python 桥接工作,同时保持单一规则所有者。本切片把当前 Todo 的完整来源读取和来源字段组装并入现有 `work_item.context.project` 请求。Goal 文档、User 作用域和 acceptance/oracle 的 IO 继续由 Python provider 执行。 + +| 边界 | 结果 | +| --- | --- | +| 迁移前 | Python 读取完整 canonical 集合,选出一个 active Todo,补充正文版本,然后发给 TS 上下文投影器。 | +| 迁移后 | `readAndProjectInteractionWorkContext` 在进程内复用现有 canonical 快照读取器,再调用原有上下文校验与投影。 | +| 退役 | 删除 Python 的 selected-Todo 分支和来源读取 import。包含它的适配器仍有真实 Goal/acceptance IO 调用方,因此保留。 | +| 完整性 | 保留同一个只读既有 store 路由、完整快照校验、分页与版本约束、归档/缺失拒绝、原始正文、规范化正文版本、状态与认领检查。不增加 Markdown fallback。 | +| 传输 | 不新增 RPC。现有上下文投影接收 `source_context`;canonical 读取沿用原有 15 秒预算。其余 Python provider 两侧仍只有两个粗粒度上下文请求。 | +| 关闭路径 | 没有 selected-Todo required read 时,不打开 provider。已提供来源的原生调用方继续使用原有纯投影。 | +| 恢复 | 正文在选择后改变时,File/SQLite 的公开 CLI 均拒绝交付。绕过 typed 来源门禁后,两条独立回归测试均失败。 | +| 权限 | 本次读取不授予认领、lease、执行、验收或写入权限。不迁移持久格式或活跃 Goal 数据。 | + +## 证据与范围 + +初始不可变基线为 `ecbf905402ccfd9e4d75b2b963b52376b4ef7332`。更新主干后的集成和最终准确 head 证据写入 PR。四项 selected-work 测试在该基线也失败,原因是旧 capture/输出布局断言。已有 PR #7430 负责修复,本切片不放宽这些断言。 + +保留的检查位于 `tests/control_plane_ts/interaction_context_readback.test.ts` 和 `tests/control_plane/test_selected_work_requirements.py`。检查覆盖完整正文与独立备注、正文版本、变更/归档/缺失/损坏/不可用来源、不创建 provider、关闭路径和 canonical 数据不变。wheel/sdist 的实际执行、完整 CLI 匹配成本与实际源码版本在 PR 单列;测试数量和更少请求不能证明整个 Goal 的成果价值。 + +共享的规范化正文版本函数同时服务原有 canonical 展示 codec 与新的来源读取。Python 不重建规则所有者。只有文档、User 作用域和 oracle provider 都有真实原生调用方后,才退役其余 Python 上下文适配器;语言本身不是删除条件。 diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index f30134a160..f7bffb6178 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -2846,3 +2846,4 @@ with shared write/read status-phase checks. See the [2026-10-02 checkpoint](ledg - Complete adaptive child admission decision: [2026-11-02 checkpoint](ledger/typescript-control-plane-migration-v0/2026-11-02-adaptive-child-admission.md). - Progress-review receipt qualification: [2026-11-02 checkpoint](ledger/typescript-control-plane-migration-v0/2026-11-02-progress-review-qualification.md). - Global Todo readiness complete owner cutover (both consumers): [2026-11-02 ledger](ledger/typescript-control-plane-migration-v0/2026-11-02-global-todo-readiness.md). +- Selected Todo source read inside the context transaction: [2026-11-04](ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md). diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index ac9c783314..c507c692b1 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -2111,3 +2111,4 @@ SQLite 中位数略升,收益不能外推为全面加速。这只资格化本 - 完整自适应子 Agent 准入决策:[2026-11-02 检查点](ledger/typescript-control-plane-migration-v0/2026-11-02-adaptive-child-admission.zh-CN.md)。 - 进度评审 receipt 资格判定:[2026-11-02 检查点](ledger/typescript-control-plane-migration-v0/2026-11-02-progress-review-qualification.zh-CN.md)。 - Global Todo 就绪分类完整 owner 切换(两个消费者):[2026-11-02 记录](ledger/typescript-control-plane-migration-v0/2026-11-02-global-todo-readiness.zh-CN.md)。 +- 当前 Todo 来源读取并入上下文事务:[2026-11-04](ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md). diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index d9a5207d39..3b4f6a61d1 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -782,7 +782,7 @@ export function createEffectRuntimeHandlers( ["work_item.interaction_reads.project", lazyHandler(() => import("./quota/turn_envelope.ts"), ({projectInteractionReadsAndGuidance}) => projectInteractionReadsAndGuidance)], ["work_item.blocked_wait_observation.project", lazyHandler(() => import("./work_items/interaction_contract.ts"), ({projectBlockedSuccessorWaitObservation}) => projectBlockedSuccessorWaitObservation)], ["work_item.context.plan", lazyHandler(() => import("./work_items/interaction_contract.ts"), ({planInteractionWorkContext}) => planInteractionWorkContext)], - ["work_item.context.project", lazyHandler(() => import("./work_items/interaction_contract.ts"), ({projectInteractionWorkContext}) => projectInteractionWorkContext)], + ["work_item.context.project", lazyHandler(() => import("./work_items/interaction_contract.ts"), ({readAndProjectInteractionWorkContext}) => readAndProjectInteractionWorkContext)], ["work_item.replan_settlement.project", lazyHandler(() => import("./work_items/replan_settlement.ts"), ({projectReplanSettlementContract}) => projectReplanSettlementContract)], ["work_item.replan_semantics.project", lazyHandler(() => import("./work_items/replan_semantics.ts"), ({projectReplanWriteback}) => projectReplanWriteback)], ["work_item.replan_context.project", lazyHandler(() => import("./work_items/replan_context.ts"), ({projectReplanContext}) => projectReplanContext)], diff --git a/loopx/control_plane/todos/read_codec.ts b/loopx/control_plane/todos/read_codec.ts index 9a8086deeb..03a2e74bfb 100644 --- a/loopx/control_plane/todos/read_codec.ts +++ b/loopx/control_plane/todos/read_codec.ts @@ -196,6 +196,10 @@ export function canonicalTodoReadItems(values: JsonObject[]): JsonObject[] { }); } +export function normalizedTodoTextRevision(normalizedText: string): string | null { + return normalizedText ? "sha256:" + hash(normalizedText) : null; +} + export function structureCanonicalTodo(item: JsonObject, role: unknown, section: unknown, limit: number | null = 500, revision = true): JsonObject { const full = compactPythonWhitespace(text(item.text)), body = compactTodoReadText(full, limit); @@ -205,7 +209,7 @@ export function structureCanonicalTodo(item: JsonObject, role: unknown, section: return {...readMetadata(item), schema_version: "todo_item_v0", role, source_section: section, archive_state: item.archive_state ?? "active", status, done: status === "done" || status === "deferred", text: body, task_class: taskClass({...item, text: body}), - ...(revision && full ? {content_revision: "sha256:" + hash(full)} : {}), + ...(revision && full ? {content_revision: normalizedTodoTextRevision(full)} : {}), ...(parsed.priority ? {priority: parsed.priority, title: compactTodoReadText(parsed.title, limit)} : {})}; } diff --git a/loopx/control_plane/work_items/context_readback.py b/loopx/control_plane/work_items/context_readback.py index 9bcfea8d13..5a9f427273 100644 --- a/loopx/control_plane/work_items/context_readback.py +++ b/loopx/control_plane/work_items/context_readback.py @@ -11,13 +11,12 @@ from ...history import load_registry from ..effect_runtime import effect_runtime_result -from ..coordination.local_authority import read_canonical_todos_if_promoted +from ..coordination.local_authority import LOCAL_COORDINATION_TODO_LIST_TIMEOUT_SECONDS from ..goals.acceptance import inspect_goal_acceptance from ..goals.state_resolution import resolve_goal_state from ..todos.list_readback import list_goal_todos from ..todos.machine_region import find_todo_regions, visible_markdown_lines from ..scheduler.monitor_batch_receipts import is_monitor_receipt_line -from ..todos.summary_item import todo_text_content_revision def _source_content(read, *, registry_path, runtime_root, goal_id, todo_id, agent_id=None, @@ -74,37 +73,6 @@ def _source_content(read, *, registry_path, runtime_root, goal_id, todo_id, agen # through the bounded acceptance RPC before discarding it here. return inspect_goal_acceptance(registry_path=registry_path, goal_id=goal_id, runtime_root=str(runtime_root), todo_id=todo_id, context_agent_id=agent_id, include_work_details=False) - if source == "selected_todo": - if not todo_id: - raise ValueError("selected Todo identity is unavailable") - canonical = read_canonical_todos_if_promoted( - runtime_root=runtime_root, goal_id=goal_id) - records = canonical["todos"] - source_name = canonical["source_authority"] - authority_read = { - "source_authority": source_name, - "provider_revision": canonical.get("provider_revision"), - "decision_read_from_provider": True, - "legacy_fallback_used": False, - } - matches = [item for item in records - if item.get("todo_id") == todo_id - and item.get("archive_state", "active") != "archive"] - todo = dict(matches[0]) if len(matches) == 1 else None - if isinstance(todo, dict) and "status" not in todo: - todo["status"] = "done" if todo.get("done") is True else "open" - if isinstance(todo, dict) and isinstance(todo.get("text"), str): - revision = todo_text_content_revision(todo["text"]) - if revision: - todo["content_revision"] = revision - return { - "ok": True, - "matched": len(matches) == 1, - "ambiguous": len(matches) > 1, - "todo": todo, - "source": source_name, - "authority_read": authority_read, - } raise ValueError("unregistered work context source") @@ -142,7 +110,7 @@ def attach_work_context(payload: dict[str, Any], *, registry_path: Path, selected["_context_text_sha256"] = selected_text_snapshot results = [dict(item) for item in (hook_dispatch or {}).get("contexts", [])] for read in reads: - if read.get("source") not in {"goal_state", "goal_acceptance", "selected_todo"}: + if read.get("source") not in {"goal_state", "goal_acceptance"}: continue result = {"command": read["command"]} try: @@ -163,8 +131,12 @@ def attach_work_context(payload: dict[str, Any], *, registry_path: Path, "required_reads": reads, "source_results": results, "selected_todo": plan.get("selected_todo"), "user_todos": users, "hook_dispatch": hook_dispatch, + "source_context": {"runtime_root": str(runtime_root.expanduser().resolve(strict=False)), "goal_id": goal_id}, **({"require_full_goal_source": True} if inline_goal_source else {}), - }, large_local_snapshot=True) + }, large_local_snapshot=True, **( + {"timeout": LOCAL_COORDINATION_TODO_LIST_TIMEOUT_SECONDS} + if any(read.get("source") == "selected_todo" for read in reads) else {} + )) # An older runtime must not silently ignore the requested delivery profile. profile = projected.pop("source_delivery_profile", None) if inline_goal_source and profile != "complete_goal_v1": diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index 02307410ab..4f4450eeb9 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -20,6 +20,7 @@ import type { JsonObject } from "../effect_program.ts"; import { AGENT_SCOPE_FRONTIER_ACTIONS, AgentScopeFrontierAction } from "../agents/agent_scope_frontier.generated.ts"; import { pendingAutomaticResumeCondition } from "../goals/vision_wait_coverage.ts"; import {compactTodoContextSource} from "../quota/context_delivery.ts"; +import {compactPythonWhitespace} from "../coordination/python_whitespace.ts"; function exactBlockedSuccessorWait(request: JsonObject, monitorWait = false): JsonObject | null { const wait = jsonObject(request.vision_wait_state); @@ -855,6 +856,55 @@ export function assertGoalContextDelivered(channel: JsonObject): void { /** Deliver full registered sources once. Ordinary mixed Goals retain progressive * reads; native delivery can require the complete original. */ +async function readSelectedTodoContext(source: JsonObject, todoId: unknown): Promise { + if (typeof todoId !== "string" || !todoId) return {error_code: "context_source_unavailable"}; + const {readCanonicalSnapshot, CANONICAL_SNAPSHOT_PAGE_REQUEST} = + await import("../coordination/canonical_snapshot_page.ts"); + const {normalizedTodoTextRevision} = await import("../todos/read_codec.ts"); + const matches: JsonObject[] = []; + let after: unknown = null; + let page: JsonObject; + do { + page = await readCanonicalSnapshot({schema_version: CANONICAL_SNAPSHOT_PAGE_REQUEST, + runtime_root: requireNonEmptyString(source.runtime_root, "runtime_root"), + goal_id: requireNonEmptyString(source.goal_id, "goal_id"), + include_leases: false, projection_readback: null, after}); + if (page.status !== "page" || !Array.isArray(page.todos)) return {error_code: "context_source_unavailable"}; + matches.push(...(page.todos as JsonObject[]).filter(todo => + todo.todo_id === todoId && todo.archive_state !== "archive")); + after = page.next; + } while (after !== null); + const todo = matches.length === 1 ? {...matches[0]} : null; + if (todo !== null) { + if (!Object.hasOwn(todo, "status")) todo.status = todo.done === true ? "done" : "open"; + if (typeof todo.text === "string") { + const revision = normalizedTodoTextRevision(compactPythonWhitespace(todo.text)); + if (revision) todo.content_revision = revision; + } + } + return {content: {ok: true, matched: matches.length === 1, ambiguous: matches.length > 1, todo, + source: page.source_authority, + authority_read: {source_authority: page.source_authority, + provider_revision: (page.snapshot as JsonObject).provider_revision, + decision_read_from_provider: true, legacy_fallback_used: false}}}; +} + +export async function readAndProjectInteractionWorkContext(request: JsonObject): Promise { + if (request.source_context === undefined) return projectInteractionWorkContext(request); + if (!Array.isArray(request.required_reads) || !Array.isArray(request.source_results)) { + throw new EffectRuntimeRequestError("context reads and source results must be arrays"); + } + const reads = request.required_reads.map(value => requireJsonObject(value, "required read")) + .filter(read => read.source === "selected_todo"); + if (reads.length === 0) return projectInteractionWorkContext(request); + const source = requireJsonObject(request.source_context, "work context source"); + const selected = jsonObject(request.selected_todo); + const observed = await readSelectedTodoContext(source, selected?.todo_id); + return projectInteractionWorkContext({...request, source_results: [ + ...request.source_results, ...reads.map(read => ({command: read.command, ...observed})), + ]}); +} + export function projectInteractionWorkContext(request: JsonObject): JsonObject { if (request.require_full_goal_source !== undefined && typeof request.require_full_goal_source !== "boolean") { throw new EffectRuntimeRequestError("Goal source delivery requirement must be boolean"); diff --git a/tests/control_plane/test_selected_work_requirements.py b/tests/control_plane/test_selected_work_requirements.py index dd47fa448c..04dc7e1c12 100644 --- a/tests/control_plane/test_selected_work_requirements.py +++ b/tests/control_plane/test_selected_work_requirements.py @@ -12,10 +12,9 @@ import pytest from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime -from loopx.control_plane.coordination.local_authority import CanonicalAuthorityRequired from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.testing.canary_harness import run_json_cli_result, write_fixture_registry -from loopx.control_plane.work_items.context_readback import _source_content +from loopx.control_plane.effect_runtime import effect_runtime_result from loopx.control_plane.todos.active_state_todo_parser import parse_todo_source from loopx.control_plane.todos.goal_todo_projection import retained_todo_summary_fields from loopx.control_plane.turn_driver.codex_cli import _prompt @@ -35,15 +34,14 @@ def test_selected_todo_refuses_unmigrated_source(tmp_path: Path) -> None: ) original = state.read_bytes() - with pytest.raises(CanonicalAuthorityRequired) as refused: - _source_content( - {"source": "selected_todo"}, - registry_path=tmp_path / "registry.json", - runtime_root=tmp_path / "runtime", - goal_id="goal-current", - todo_id="todo_current", - ) - assert refused.value.code == "canonical_authority_required" + result = effect_runtime_result("work_item.context.project", { + "required_reads": [{"source": "selected_todo", "command": "read-selected"}], + "source_results": [], "selected_todo": {"todo_id": "todo_current"}, + "source_context": {"runtime_root": str(tmp_path / "runtime"), "goal_id": "goal-current"}, + }) + context = result["work_context"] + assert context["complete"] is False + assert context["failures"][0]["error_code"] == "context_source_unavailable" assert state.read_bytes() == original @@ -368,14 +366,13 @@ def test_selected_todo_body_change_between_selection_and_readback_blocks_deliver ) from loopx.control_plane.work_items import context_readback - read_source = context_readback._source_content run_effect = context_readback.effect_runtime_result projected_request = {} changed = False - def mutate_after_selection(read, **kwargs): + def capture_projection(effect, request, **kwargs): nonlocal changed - if read.get("source") == "selected_todo" and not changed: + if effect == "work_item.context.project" and not changed: update_code, update = run_json_cli_result( "todo", "update", "--goal-id", "selected-race-goal", "--todo-id", "todo_selected_work", "--agent-id", "agent-a", @@ -383,14 +380,10 @@ def mutate_after_selection(read, **kwargs): ) assert update_code == 0, update changed = True - return read_source(read, **kwargs) - - def capture_projection(effect, request, **kwargs): if effect == "work_item.context.project": projected_request.update(request) return run_effect(effect, request, **kwargs) - monkeypatch.setattr(context_readback, "_source_content", mutate_after_selection) monkeypatch.setattr(context_readback, "effect_runtime_result", capture_projection) from loopx.cli import build_parser from loopx.cli_commands.quota import handle_quota_command @@ -413,12 +406,12 @@ def capture_payload(payload, *_args): assert changed assert packet["selected_todo"]["todo_id"] == "todo_selected_work" selected_revision = projected_request["selected_todo"]["content_revision"] - readback = next( - result for result in projected_request["source_results"] - if result.get("content", {}).get("todo", {}).get("todo_id") == "todo_selected_work" - ) assert selected_revision - assert readback["content"]["todo"]["content_revision"] != selected_revision + current_code, current = run_json_cli_result( + "todo", "list", "--goal-id", "selected-race-goal", "--todo-id", "todo_selected_work", + registry_path=registry, runtime_root=runtime, + ) + assert current_code == 0 and current["todo"]["text"] == changed_text, current channel = packet["interaction_contract"]["agent_channel"] assert channel["work_context"]["complete"] is False assert channel["delivery_allowed"] is False diff --git a/tests/control_plane_ts/interaction_context_readback.test.ts b/tests/control_plane_ts/interaction_context_readback.test.ts new file mode 100644 index 0000000000..1b4eb5f685 --- /dev/null +++ b/tests/control_plane_ts/interaction_context_readback.test.ts @@ -0,0 +1,80 @@ +import assert from "node:assert/strict"; +import {createHash} from "node:crypto"; +import {mkdtemp, readdir, rm, stat} from "node:fs/promises"; +import {tmpdir} from "node:os"; +import {join} from "node:path"; +import test from "node:test"; + +import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; +import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_authority_store.ts"; +import {readAndProjectInteractionWorkContext, projectInteractionWorkContext} + from "../../loopx/control_plane/work_items/interaction_contract.ts"; +import {authorityProjectionFixture} from "./authority_projection_fixture.ts"; +import {engageCanonicalRoute} from "./local_promotion_fixture.ts"; + +const goal = "context-read"; +const text = "Keep complete requirements. ".repeat(60) + "Stop before deployment."; +const revision = "sha256:" + createHash("sha256").update(text.trim()).digest("hex"); + +function request(root: string): JsonObject { + return {required_reads: [{source: "selected_todo", command: "read-selected"}], source_results: [], + selected_todo: {todo_id: "todo_selected", status: "open", claimed_by: "agent-a", + text: "Keep complete requirements.", content_revision: revision, + _context_text_sha256: createHash("sha256").update(text).digest("hex")}, + source_context: {runtime_root: root, goal_id: goal}}; +} + +for (const state of ["active", "archive", "missing", "changed", "invalid_read_model"] as const) { + test(`canonical selected context: ${state}`, async t => { + const root = await mkdtemp(join(tmpdir(), "selected-context-")); + t.after(() => rm(root, {recursive: true, force: true})); + await engageCanonicalRoute(root, goal); + const store = new FileAuthorityStore(join(root, "authority", "file-v0"), goal); + const todo = {todo_id: "todo_selected", role: "agent", status: "open", done: false, + text: state === "changed" ? text + " Changed requirement." : text, + archive_state: state === "archive" ? "archive" : "active", task_class: "advancement_task", + claimed_by: "agent-a", note: "Retain this independent note."}; + const projection = authorityProjectionFixture(goal, state === "missing" ? [] : [todo]); + if (state === "invalid_read_model") (projection.todo_read_model as JsonObject).records_sha256 = "0".repeat(64); + const seeded = await store.commitAuthority({operation_id: "seed-context", expected_provider_revision: null, + next_projection: projection, + events: [], receipts: []}); + assert.equal(seeded.status, "applied"); + const before = await store.loadAuthority(); + const input = request(root), original = structuredClone(input); + const result = await readAndProjectInteractionWorkContext(input); + const context = result.work_context as JsonObject; + assert.equal(context.complete, state === "active"); + if (state === "active") { + const content = ((context.sources as JsonObject[])[0].content as JsonObject); + assert.equal((content.todo as JsonObject).text, text); + assert.equal((content.todo as JsonObject).note, todo.note); + assert.equal((content.todo as JsonObject).content_revision, revision); + assert.equal((content.authority_read as JsonObject).provider_revision, seeded.provider_revision); + assert.equal(context.selected_todo_text_verified, true); + } else { + assert.equal((context.failures as JsonObject[])[0].error_code, "context_source_unavailable"); + } + assert.deepEqual(await store.loadAuthority(), before); + assert.deepEqual(input, original); + }); +} + +test("context without a selected source performs no provider read", async t => { + const directory = await mkdtemp(join(tmpdir(), "context-no-read-")); + t.after(() => rm(directory, {recursive: true, force: true})); + const root = join(directory, "absent-runtime"); + const input = {...request(root), required_reads: []}; + assert.deepEqual(await readAndProjectInteractionWorkContext(input), projectInteractionWorkContext(input)); + await assert.rejects(stat(root), {code: "ENOENT"}); +}); + +test("an unavailable canonical store is not recreated by context reading", async t => { + const root = await mkdtemp(join(tmpdir(), "context-missing-store-")); + t.after(() => rm(root, {recursive: true, force: true})); + await engageCanonicalRoute(root, goal); + const before = await readdir(root, {recursive: true}); + const result = await readAndProjectInteractionWorkContext(request(root)); + assert.equal((result.work_context as JsonObject).complete, false); + assert.deepEqual(await readdir(root, {recursive: true}), before); +}); From 0eff8370a08d1c17e55e84743562b269b8f151da Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:59:09 +0800 Subject: [PATCH 2/3] docs(context): disclose source qualification and performance hold Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- .../2026-11-04-selected-context-source-read.md | 15 ++++++++++++++- ...26-11-04-selected-context-source-read.zh-CN.md | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md index c0326a400f..6cac3bb10c 100644 --- a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md @@ -8,7 +8,7 @@ The accepted request is to retire Python bridge work without creating parallel s | After | `readAndProjectInteractionWorkContext` uses the existing canonical snapshot reader in-process, then invokes the unchanged context validator/projector. | | Retirement | The Python selected-Todo branch and source-reader imports are removed. The containing adapter remains for its real Goal/acceptance IO callers. | | Integrity | The same existing-only store route, complete snapshot validation, page/revision fencing, archived/missing refusal, raw body, normalized content revision, status and claim checks remain. There is no Markdown fallback. | -| Transport | No RPC method is added. The existing context projection receives `source_context`; the canonical read retains its existing 15-second budget. Source delivery still uses two coarse context requests around the remaining Python providers. | +| Transport | No RPC method is added. The existing context projection receives `source_context`; the canonical read retains its existing 15-second budget. The existing context plan/project phases remain; the remaining Python providers keep their own calls. The selected-Todo snapshot no longer crosses separately. | | Feature off | A request without a selected-Todo required read does not open a provider. Prepared-source native callers retain the existing pure projection. | | Recovery | A changed selected body fails the public CLI on both File and SQLite. Bypassing the typed source guard makes both independent regression cases fail. | | Authority | This read grants no claim, lease, execution, acceptance or write authority. No persisted format or live Goal data is migrated. | @@ -20,3 +20,16 @@ The immutable initial baseline is `ecbf905402ccfd9e4d75b2b963b52376b4ef7332`; la Durable checks live in `tests/control_plane_ts/interaction_context_readback.test.ts` and `tests/control_plane/test_selected_work_requirements.py`. They cover complete requirements/notes, source revision, changed/archived/missing/corrupt/unavailable state, no provider creation, feature-off behavior and unchanged canonical data. Wheel and sdist execution, matched full-CLI costs and actual source revision are recorded separately in the PR; test counts or fewer requests do not prove whole-Goal outcome value. The shared normalized-text revision helper serves the existing canonical display codec and the new source read. No policy is reimplemented in Python. Retire the remaining Python context adapter only when its document, User-scope and oracle providers have real native callers; removing a file based on its language is not the exit criterion. + +## Final source proposal qualification + +Integration base: `d29ec8bf2a105aac24629bcbf3dfb7c97aea51fe`. Source candidate: `c137daecc56a5f1784097742b212d26b27f83892` (subsequent documentation updates do not change the four production files). + +- Python adapter: +7/-35 lines, net -28. Total production diff: +63/-37, net +26. This is source/transport consolidation, not a claim that previously Python-owned domain policy moved. +- Source checks: 138 focused/related Python tests and 93 native tests pass; four older capture-layout failures remain baseline evidence. Mutation of the source guard breaks both real File/SQLite body-change regressions. +- Fresh wheel and sdist: File and SQLite command readbacks pass in four isolated installation cases, with source-file hashes checked. +- The actual `loopx` entrypoint on SQLite, with 500 historical records and 30 alternating matched pairs, has p50 647→657 ms and p95 779→799 ms. The default entrypoint stays within the declared 5%/25 ms regression bounds; no speedup is claimed. +- The compatibility `python -m loopx.cli` entrypoint has paired p95 1309→1486 ms. This remains an unresolved performance hold. The observed imports/parser hotspots do not prove the tail's cause; raw results and earlier samples are retained. Complete cost/Host adoption qualification is not certified by this proposal. +- Complete inline work context matches in all 30 default-entry pairs. Event IDs are identical with only the event-clock input fixed. Differences in collection clocks and their computed ages remain in raw observations; no ownership, body, field, refusal or identity is normalized. + +**Author implementation is complete; merge qualification remains held for performance review and a fresh exact-head review.** No merge, deployment, authority migration or persistent-state deletion is performed. diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md index c8e903dc9c..c536974708 100644 --- a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md @@ -8,7 +8,7 @@ | 迁移后 | `readAndProjectInteractionWorkContext` 在进程内复用现有 canonical 快照读取器,再调用原有上下文校验与投影。 | | 退役 | 删除 Python 的 selected-Todo 分支和来源读取 import。包含它的适配器仍有真实 Goal/acceptance IO 调用方,因此保留。 | | 完整性 | 保留同一个只读既有 store 路由、完整快照校验、分页与版本约束、归档/缺失拒绝、原始正文、规范化正文版本、状态与认领检查。不增加 Markdown fallback。 | -| 传输 | 不新增 RPC。现有上下文投影接收 `source_context`;canonical 读取沿用原有 15 秒预算。其余 Python provider 两侧仍只有两个粗粒度上下文请求。 | +| 传输 | 不新增 RPC。现有上下文投影接收 `source_context`;canonical 读取沿用原有 15 秒预算。原有上下文 plan/project 阶段保留;其余 Python provider 仍有各自的调用。当前 Todo 快照不再单独跨运行时传输。 | | 关闭路径 | 没有 selected-Todo required read 时,不打开 provider。已提供来源的原生调用方继续使用原有纯投影。 | | 恢复 | 正文在选择后改变时,File/SQLite 的公开 CLI 均拒绝交付。绕过 typed 来源门禁后,两条独立回归测试均失败。 | | 权限 | 本次读取不授予认领、lease、执行、验收或写入权限。不迁移持久格式或活跃 Goal 数据。 | @@ -20,3 +20,16 @@ 保留的检查位于 `tests/control_plane_ts/interaction_context_readback.test.ts` 和 `tests/control_plane/test_selected_work_requirements.py`。检查覆盖完整正文与独立备注、正文版本、变更/归档/缺失/损坏/不可用来源、不创建 provider、关闭路径和 canonical 数据不变。wheel/sdist 的实际执行、完整 CLI 匹配成本与实际源码版本在 PR 单列;测试数量和更少请求不能证明整个 Goal 的成果价值。 共享的规范化正文版本函数同时服务原有 canonical 展示 codec 与新的来源读取。Python 不重建规则所有者。只有文档、User 作用域和 oracle provider 都有真实原生调用方后,才退役其余 Python 上下文适配器;语言本身不是删除条件。 + +## 最终源码候选资格 + +集成基线:`d29ec8bf2a105aac24629bcbf3dfb7c97aea51fe`。源码候选:`c137daecc56a5f1784097742b212d26b27f83892`;后续文档更新不改变四个生产文件。 + +- Python 适配器新增7行、删除35行,净减28行。生产代码合计新增63行、删除37行,净增26行。本次收敛来源与传输工作,不声称迁走了原本由 Python 持有的领域规则。 +- 138项聚焦/相关 Python 测试和93项原生测试通过。四项旧 capture 布局失败保留为基线证据。绕过来源门禁后,File/SQLite 两条真实正文变更回归均失败。 +- wheel/sdist 在独立环境执行,File/SQLite 四个实际命令读回通过,并核对源码文件哈希。 +- 实际 `loopx` 入口使用 SQLite、500条历史记录和30对交错匹配样本:p50 为647→657 ms,p95 为779→799 ms。默认入口在既有5%/25 ms回退范围内,不声称加速。 +- 兼容的 `python -m loopx.cli` 入口,匹配样本 p95 为1309→1486 ms,仍属未闭合的性能阻断。已观察到的导入/参数注册热点不能证明尾延迟的根因;原始结果与较早样本全部保留。本候选不认证完整成本或真实 Host 采用。 +- 默认入口30对样本的完整内联工作上下文一致。仅固定事件时钟输入后,事件ID一致。采集时钟和计算出的age差异保留在原始观察中,不归一化身份、归属、正文、字段或拒绝结果。 + +**作者实现已完成;性能复核和最终准确 head 的复审完成前,继续阻断合并。** 本轮不执行合并、部署、authority迁移或持久数据删除。 From 6811fd2d8c3c5bbde9fa11d67c551f4a41daaa03 Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:08:40 +0800 Subject: [PATCH 3/3] docs(context): bind latest capture integration evidence Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- .../2026-11-04-selected-context-source-read.md | 6 ++++++ .../2026-11-04-selected-context-source-read.zh-CN.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md index 6cac3bb10c..762de909ec 100644 --- a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.md @@ -33,3 +33,9 @@ Integration base: `d29ec8bf2a105aac24629bcbf3dfb7c97aea51fe`. Source candidate: - Complete inline work context matches in all 30 default-entry pairs. Event IDs are identical with only the event-clock input fixed. Differences in collection clocks and their computed ages remain in raw observations; no ownership, body, field, refusal or identity is normalized. **Author implementation is complete; merge qualification remains held for performance review and a fresh exact-head review.** No merge, deployment, authority migration or persistent-state deletion is performed. + +### Later upstream capture integration + +Candidate `ff4e94459` integrates latest base `ce4cfc1ce`. Its four changed production files remain byte-identical to `c137daecc`. Upstream now returns capture navigation; the existing product reader verifies and opens the complete current projection. Actual File/SQLite entrypoint readbacks on this base preserve complete work context and event identity; logical Effect calls are 74→73 in these newer observations. + +Six historical direct-stdout assertions fail on both latest main and candidate because they still expect an inline `should_run` field. Both selected runs have 2 pass /6 fail, with the same identities and failure boundary. Those failures remain evidence. Earlier installed/cost measurements retain their stated revision; latest-main full cost and Host qualification remain held. diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md index c536974708..c1eeda5117 100644 --- a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-04-selected-context-source-read.zh-CN.md @@ -33,3 +33,9 @@ - 默认入口30对样本的完整内联工作上下文一致。仅固定事件时钟输入后,事件ID一致。采集时钟和计算出的age差异保留在原始观察中,不归一化身份、归属、正文、字段或拒绝结果。 **作者实现已完成;性能复核和最终准确 head 的复审完成前,继续阻断合并。** 本轮不执行合并、部署、authority迁移或持久数据删除。 + +### 后续上游 capture 集成 + +候选 `ff4e94459` 集成最新基线 `ce4cfc1ce`,四个改动生产文件与 `c137daecc` 字节一致。上游现在返回 capture 导航引用;原有产品读取器校验后打开完整当前观察。在该基线上,实际 File/SQLite 入口的完整工作上下文与事件身份保持一致;业务 Effect 调用在这组较新观察中为74→73。 + +六项旧断言仍直接读取 stdout 顶层 `should_run`,因此最新 main 和候选均为2通过/6失败,失败身份与边界相同。失败继续保留。此前安装包和成本测量保留各自版本;不声称最新主干的完整成本或 Host 资格已通过。