From 8bc4890d0d227239a27794ff6babf968046bd4ad Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:38:27 +0530 Subject: [PATCH 1/5] feat(context): include suggested acceptance bodies in initial reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../quota/progressive_context.ts | 114 +++++++++++++++++- loopx/control_plane/quota/turn_envelope.ts | 91 +------------- .../control_plane/test_context_first_read.py | 81 +++++++++++++ .../test_outcome_context_navigation_cli.py | 22 ++++ 4 files changed, 218 insertions(+), 90 deletions(-) diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index f2d6a8bf7d..e60e517ac0 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -2,7 +2,7 @@ * addressed in the immutable source. This plan changes no canonical state. */ import type {JsonObject} from "../effect_program.ts"; import {jsonObject, requireJsonObject} from "../runtime_decode.ts"; -import {GOAL_OUTCOMES_SCHEMA} from "../goals/outcome_requirements.ts"; +import {GOAL_OUTCOMES_SCHEMA, OUTCOME_CONTEXT_NAVIGATION_SCHEMA} from "../goals/outcome_requirements.ts"; import {FULL_OUTCOME_READ_GUIDANCE, PROGRESSIVE_OUTCOME_READ_GUIDANCE} from "../work_items/interaction_contract.ts"; import {quotaSummaryReferences, contextSourceIdentity, contextProjectionChanges, type ProjectionSourceValues} from "./diagnostic_projection.ts"; @@ -193,3 +193,115 @@ export function prepareProgressiveContextDelivery(request: JsonObject, legacyFie const {operation: _operation, source_edits: _edits, source_number_tokens: _numbers, ...source} = request; return contextProjectionChanges(source, {context: required.context, delivery, projected: required.projected}, sourceValues); } + +export function initialContextReadPointers(context: JsonObject): string[] { + const object = (value: unknown): JsonObject | undefined => + value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : undefined; + const interaction = object(context.interaction_contract); + // Unknown envelopes retain generic discovery instead of guessing their policy. + if (interaction?.schema_version !== "loopx_interaction_contract_v0") return []; + const pointers: string[] = []; + const include = (...path: (string | number)[]): void => { + let value: unknown = context; + for (const key of path) { + if (value === null || typeof value !== "object" || !Object.hasOwn(value, key)) return; + value = (value as Record)[key]; + } + pointers.push("/" + path.map(key => String(key).replaceAll("~", "~0").replaceAll("/", "~1")).join("/")); + }; + for (const field of ["ok", "schema_version", "goal_id", "agent_id", "should_run", "effective_action", + "normal_delivery_allowed", "recovery_delivery_allowed", "self_repair_allowed", "capability_repair_allowed", "workspace_repair_allowed", + "safe_bypass_allowed", "safe_bypass_kind", "blocked_action_scope", "reason", + "execution_obligation", "goal_boundary", "heartbeat_receipt", "work_lane_contract", "selected_todo"]) + include(field); + include("agent_lane_next_action", "next_step"); + include("agent_lane_next_action", "next_action_basis"); + include("interaction_contract", "user_channel"); + include("interaction_contract", "agent_channel", "required_reads"); + const work = ["interaction_contract", "agent_channel", "work_context"]; + include(...work, "instruction"); + include(...work, "user_todos"); + const channel = object(interaction.agent_channel); + const sources = object(channel?.work_context)?.sources; + if (Array.isArray(sources)) sources.forEach((_source, index) => { + const source = [...work, "sources", index]; + for (const field of ["source", "kind", "reason", "ordering", "command"]) + include(...source, field); + // Current Goal/Todo text and source obligations stay whole. Expansive + // evidence pages, example recipes and historical bodies remain on demand. + include(...source, "content", "text"); + include(...source, "content", "instruction"); + for (const field of ["ok", "status", "reason", "error", "recovery", "revision", "provider_revision", "source_authority", + "decision_read_from_provider", "legacy_fallback_used", "graph_enabled", "harness_enabled", "boundary", "authority_read"]) + include(...source, "content", field); + include(...source, "content", "current", "revision"); + include(...source, "content", "current", "instruction"); + include(...source, "content", "outcome_context"); + const sourceRow = object(_source), content = object(sourceRow?.content); + const requirements = object(object(content?.goal_acceptance_contract)?.outcome_requirements); + const outcomes = requirements?.outcomes, navigation = object(content?.outcome_context); + const identityAgent = object(context.agent_identity)?.agent_id; + const agentId = context.agent_id ?? identityAgent; + const suggested = navigation?.suggested_outcome_ids; + // Reuse the acceptance owner's relationship navigation; do not infer scope + // from ownership or prose. This only expands the starting read: other + // outcomes and the original required-read obligations remain available. + const expand = sourceRow?.source === "goal_acceptance" && content?.ok !== false + && content?.status === "loaded" && content.decision_read_from_provider === true + && content.legacy_fallback_used === false && content.read_complete !== false + && requirements?.schema_version === GOAL_OUTCOMES_SCHEMA && requirements.current_lifetime === true + && navigation?.schema_version === OUTCOME_CONTEXT_NAVIGATION_SCHEMA && navigation.coverage_complete === true + && typeof context.goal_id === "string" && navigation.goal_id === context.goal_id + && typeof agentId === "string" && agentId.length > 0 && navigation.agent_id === agentId + && (context.agent_id === undefined || identityAgent === undefined || context.agent_id === identityAgent) + && typeof object(context.selected_todo)?.todo_id === "string" + && navigation.todo_id === object(context.selected_todo)?.todo_id + && navigation.outcome_revision === requirements.revision + && typeof content.provider_revision === "string" && navigation.provider_revision === content.provider_revision + && Array.isArray(outcomes) && Array.isArray(suggested) + && suggested.every(id => typeof id === "string" + && outcomes.filter(value => object(value)?.id === id).length === 1); + const expanded = new Set(expand ? suggested as string[] : []); + if (Array.isArray(outcomes)) outcomes.forEach((_outcome, outcomeIndex) => { + const outcome = [...source, "content", "goal_acceptance_contract", "outcome_requirements", "outcomes", outcomeIndex]; + const outcomeId = object(_outcome)?.id; + if (typeof outcomeId === "string" && expanded.has(outcomeId)) { include(...outcome); return; } + include(...outcome, "id"); + include(...outcome, "supporting_material_pointer"); + }); + include(...source, "content", "todo", "text"); + include(...source, "content", "todo", "note"); + include(...source, "content", "graph", "guidance"); + include(...source, "content", "graph", "recent_findings"); + include(...source, "content", "graph", "result_page"); + include(...source, "content", "harness", "guidance"); + include(...source, "content", "harness", "orchestration_gate"); + }); + for (const field of ["next_cli_actions", "spend_allowed_now", "spend_after_validation", "spend_policy", + "quota_spend_source", "delivery_workspace_causality", "vision_continuation_audit"]) + include("interaction_contract", "cli_channel", field); + const planPath = ["interaction_contract", "cli_channel", "settlement_plan"]; + const plan = object(object(interaction.cli_channel)?.settlement_plan); + if (plan?.schema_version === "quota_settlement_plan_v1" && Array.isArray(plan.ordered_steps)) { + for (const field of Object.keys(plan)) if (field !== "ordered_steps") include(...planPath, field); + plan.ordered_steps.forEach((value, stepIndex) => { + const step = object(value), stepPath = [...planPath, "ordered_steps", stepIndex]; + if (!step) { include(...stepPath); return; } + for (const field of Object.keys(step)) { + if (field !== "optional_attachments" || !Array.isArray(step[field])) { include(...stepPath, field); continue; } + step[field].forEach((attachment, attachmentIndex) => { + const path = [...stepPath, field, attachmentIndex]; + if (object(attachment)?.required !== false) { include(...path); return; } + // Explicitly optional guidance is read before using that attachment. + // Keep its identity and actual read/authoring routes discoverable. + for (const key of Object.keys(object(attachment)!)) { + if (["guidance", "attachment_template", "linked_question_attachment_template", "path_delta_attachment_template", + "optional_measurement_example", "optional_question_evolution_example"].includes(key)) continue; + include(...path, key); // Unknown attachment controls are never hidden. + } + }); + } + }); + } else include(...planPath); // Unknown plan versions keep their complete obligations. + return pointers; +} diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index ed016e090a..8677466dc6 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1,5 +1,5 @@ import {capturedContextSelection} from "./context_selection.ts"; -import {prepareProgressiveContextDelivery} from "./progressive_context.ts"; +import {initialContextReadPointers, prepareProgressiveContextDelivery} from "./progressive_context.ts"; import {contextProjectionChanges, projectQuotaDiagnostics} from "./diagnostic_projection.ts"; import type { LoopXTurnRouteValue } from "../turn_driver/turn_contract_generated.ts"; import { EffectiveAction, type QuotaEffectiveActionValue } from "./effective_action.generated.ts"; @@ -1406,99 +1406,12 @@ export function turnEnvelopeRoute(envelope: JsonObject, historicalText?: JsonObj } /** Navigation over captured facts, never a summary or a source-read receipt. */ -function initialReadPointers(context: JsonObject): string[] { - const object = (value: unknown): JsonObject | undefined => - value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : undefined; - const interaction = object(context.interaction_contract); - // Unknown envelopes retain generic discovery instead of guessing their policy. - if (interaction?.schema_version !== "loopx_interaction_contract_v0") return []; - const pointers: string[] = []; - const include = (...path: (string | number)[]): void => { - let value: unknown = context; - for (const key of path) { - if (value === null || typeof value !== "object" || !Object.hasOwn(value, key)) return; - value = (value as Record)[key]; - } - pointers.push("/" + path.map(key => String(key).replaceAll("~", "~0").replaceAll("/", "~1")).join("/")); - }; - for (const field of ["ok", "schema_version", "goal_id", "agent_id", "should_run", "effective_action", - "normal_delivery_allowed", "recovery_delivery_allowed", "self_repair_allowed", "capability_repair_allowed", "workspace_repair_allowed", - "safe_bypass_allowed", "safe_bypass_kind", "blocked_action_scope", "reason", - "execution_obligation", "goal_boundary", "heartbeat_receipt", "work_lane_contract", "selected_todo"]) - include(field); - include("agent_lane_next_action", "next_step"); - include("agent_lane_next_action", "next_action_basis"); - include("interaction_contract", "user_channel"); - include("interaction_contract", "agent_channel", "required_reads"); - const work = ["interaction_contract", "agent_channel", "work_context"]; - include(...work, "instruction"); - include(...work, "user_todos"); - const channel = object(interaction.agent_channel); - const sources = object(channel?.work_context)?.sources; - if (Array.isArray(sources)) sources.forEach((_source, index) => { - const source = [...work, "sources", index]; - for (const field of ["source", "kind", "reason", "ordering", "command"]) - include(...source, field); - // Current Goal/Todo text and source obligations stay whole. Expansive - // evidence pages, example recipes and historical bodies remain on demand. - include(...source, "content", "text"); - include(...source, "content", "instruction"); - for (const field of ["ok", "status", "reason", "error", "recovery", "revision", "provider_revision", "source_authority", - "decision_read_from_provider", "legacy_fallback_used", "graph_enabled", "harness_enabled", "boundary", "authority_read"]) - include(...source, "content", field); - include(...source, "content", "current", "revision"); - include(...source, "content", "current", "instruction"); - include(...source, "content", "outcome_context"); - const requirements = object(object(object(_source)?.content)?.goal_acceptance_contract)?.outcome_requirements; - const outcomes = object(requirements)?.outcomes; - if (Array.isArray(outcomes)) outcomes.forEach((_outcome, outcomeIndex) => { - const outcome = [...source, "content", "goal_acceptance_contract", "outcome_requirements", "outcomes", outcomeIndex]; - include(...outcome, "id"); - include(...outcome, "supporting_material_pointer"); - }); - include(...source, "content", "todo", "text"); - include(...source, "content", "todo", "note"); - include(...source, "content", "graph", "guidance"); - include(...source, "content", "graph", "recent_findings"); - include(...source, "content", "graph", "result_page"); - include(...source, "content", "harness", "guidance"); - include(...source, "content", "harness", "orchestration_gate"); - }); - for (const field of ["next_cli_actions", "spend_allowed_now", "spend_after_validation", "spend_policy", - "quota_spend_source", "delivery_workspace_causality", "vision_continuation_audit"]) - include("interaction_contract", "cli_channel", field); - const planPath = ["interaction_contract", "cli_channel", "settlement_plan"]; - const plan = object(object(interaction.cli_channel)?.settlement_plan); - if (plan?.schema_version === "quota_settlement_plan_v1" && Array.isArray(plan.ordered_steps)) { - for (const field of Object.keys(plan)) if (field !== "ordered_steps") include(...planPath, field); - plan.ordered_steps.forEach((value, stepIndex) => { - const step = object(value), stepPath = [...planPath, "ordered_steps", stepIndex]; - if (!step) { include(...stepPath); return; } - for (const field of Object.keys(step)) { - if (field !== "optional_attachments" || !Array.isArray(step[field])) { include(...stepPath, field); continue; } - step[field].forEach((attachment, attachmentIndex) => { - const path = [...stepPath, field, attachmentIndex]; - if (object(attachment)?.required !== false) { include(...path); return; } - // Explicitly optional guidance is read before using that attachment. - // Keep its identity and actual read/authoring routes discoverable. - for (const key of Object.keys(object(attachment)!)) { - if (["guidance", "attachment_template", "linked_question_attachment_template", "path_delta_attachment_template", - "optional_measurement_example", "optional_question_evolution_example"].includes(key)) continue; - include(...path, key); // Unknown attachment controls are never hidden. - } - }); - } - }); - } else include(...planPath); // Unknown plan versions keep their complete obligations. - return pointers; -} - // Quota field vocabulary stays with its decision owner; the RFC 6901 reader // receives addresses and remains independent of Goal/permission policy. function capturedDecisionSelection(request: JsonObject): JsonObject { const initial = request.initial_read === true || request.list_fields === true && Array.isArray(request.select) && request.select.length === 1 && request.select[0] === ""; - return capturedContextSelection(request, initial ? initialReadPointers(requireJsonObject(request.context, "captured context")) : []); + return capturedContextSelection(request, initial ? initialContextReadPointers(requireJsonObject(request.context, "captured context")) : []); } export function evaluateTurnEnvelope(value: unknown): JsonObject { diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index 79a7c25108..1e9ce6a984 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -143,6 +143,87 @@ def test_initial_read_exposes_existing_outcome_addresses_without_loading_all_cri assert json.loads(detail["content"])["selections"][0]["value"] == outcome +@pytest.fixture +def navigated_decision(decision): + decision["agent_identity"] = {"agent_id": "author"} + content = decision["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"] + decision["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["source"] = "goal_acceptance" + outcomes = [{"id": name, "owner_agent_id": owner, "revision": 1, + "requirements": [{"id": "tail", "description": text, + "future_required_fact": "Keep this unknown requirement field whole."}]} + for name, owner, text in [ + ("current", "author", "Current work must retain the final cancellation condition."), + ("dependency", "peer", "The peer dependency must be delivered before publication."), + ("independent", "peer", "Read this other complete criterion when it becomes relevant.")]] + content.update(status="loaded", ok=True, provider_revision="fixture-current", decision_read_from_provider=True, + legacy_fallback_used=False, + goal_acceptance_contract={"outcome_requirements": {"schema_version": "loopx_goal_outcomes_v0", + "current_lifetime": True, "revision": 7, "outcomes": outcomes}}, + outcome_context={"schema_version": "outcome_context_navigation_v1", "goal_id": "fixture", + "agent_id": "author", "todo_id": "todo_current", "outcome_revision": 7, + "provider_revision": "fixture-current", "coverage_complete": True, + "suggested_outcome_ids": ["current", "dependency"], + "instruction": "Navigation is guidance; other applicable criteria still require reading."}) + return decision + + +def test_initial_read_uses_current_navigation_without_hiding_peer_constraints(tmp_path, navigated_decision): + before = json.loads(json.dumps(navigated_decision)) + view = native_context_capture_view(navigated_decision, tmp_path) + page, values = read_selection(view["first_read_command"]) + prefix = "/interaction_contract/agent_channel/work_context/sources/0/content/goal_acceptance_contract/outcome_requirements/outcomes" + outcomes = before["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["goal_acceptance_contract"]["outcome_requirements"]["outcomes"] + assert values[prefix + "/0"] == outcomes[0] + assert values[prefix + "/1"] == outcomes[1] # Shared work crosses Agent ownership. + assert prefix + "/0/id" not in values # Do not repeat a delivered parent. + assert values[prefix + "/2/id"] == "independent" + assert outcomes[2]["requirements"][0]["description"] not in page["content"] + detail = read_context_capture(Namespace(capture_file=view["recovery"]["path"], + capture_sha256=view["recovery"]["sha256"], format="json", select=[prefix + "/2"], + offset=0, page_bytes=64000, max_page_output_bytes=64000)) + assert json.loads(detail["content"])["selections"][0]["value"] == outcomes[2] + assert json.loads(Path(view["recovery"]["path"]).read_bytes()) == before + assert capture_page_range(page, raw=Path(view["recovery"]["path"]).read_bytes(), digest=page["sha256"]) is None + + +@pytest.mark.parametrize("field,value", [ + ("goal_id", "other-goal"), ("agent_id", "other-agent"), ("todo_id", "other-todo"), + ("outcome_revision", 6), ("provider_revision", "older-source"), + ("coverage_complete", False), ("schema_version", "future-navigation"), + ("suggested_outcome_ids", ["current", "missing-outcome"]), +]) +def test_initial_read_does_not_expand_unbound_navigation(tmp_path, navigated_decision, field, value): + source = navigated_decision["interaction_contract"]["agent_channel"]["work_context"]["sources"][0] + source["content"]["outcome_context"][field] = value + view = native_context_capture_view(navigated_decision, tmp_path) + page, values = read_selection(view["first_read_command"]) + prefix = "/interaction_contract/agent_channel/work_context/sources/0/content/goal_acceptance_contract/outcome_requirements/outcomes" + assert values[prefix + "/0/id"] == "current" and prefix + "/0" not in values + assert "Current work must retain the final cancellation condition." not in page["content"] + assert values["/interaction_contract/agent_channel/work_context/sources/0/content/outcome_context"] == source["content"]["outcome_context"] + + +@pytest.mark.parametrize("failure", ["source_incomplete", "legacy_source", "old_lifetime", "ambiguous_outcome", "conflicting_agent"]) +def test_initial_read_keeps_uncertain_sources_on_the_existing_detail_route(tmp_path, navigated_decision, failure): + content = navigated_decision["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"] + declaration = content["goal_acceptance_contract"]["outcome_requirements"] + if failure == "source_incomplete": + content["read_complete"] = False + elif failure == "legacy_source": + content["legacy_fallback_used"] = True + elif failure == "old_lifetime": + declaration["current_lifetime"] = False + elif failure == "ambiguous_outcome": + declaration["outcomes"].append({"id": "current", "requirements": ["A different captured revision."]}) + else: + navigated_decision["agent_id"] = "different-agent" + view = native_context_capture_view(navigated_decision, tmp_path) + page, values = read_selection(view["first_read_command"]) + prefix = "/interaction_contract/agent_channel/work_context/sources/0/content/goal_acceptance_contract/outcome_requirements/outcomes" + assert values[prefix + "/0/id"] == "current" and prefix + "/0" not in values + assert "Current work must retain the final cancellation condition." not in page["content"] + + def test_small_reference_budget_omits_the_whole_initial_command(tmp_path, decision): view = native_context_capture_view(decision, tmp_path, max_page_output_bytes=3000) assert "first_read_command" not in view diff --git a/tests/control_plane/test_outcome_context_navigation_cli.py b/tests/control_plane/test_outcome_context_navigation_cli.py index 5b6df27a55..83cfed9bdf 100644 --- a/tests/control_plane/test_outcome_context_navigation_cli.py +++ b/tests/control_plane/test_outcome_context_navigation_cli.py @@ -12,6 +12,7 @@ from loopx.control_plane.todos.machine_section_projection import render_canonical_todo_sections from loopx.control_plane.turn_driver.codex_context import codex_context_hook from loopx.cli_commands.quota_capture import read_context_capture +from tests.control_plane.codex_checkpoint_fixture import reference_metadata_oracle @pytest.mark.parametrize("inherited", [False, True]) @@ -71,6 +72,7 @@ def cli(*args): *([] if inherited else ["--context-delivery", "on"]), "--runtime-profile", "generic_cli", "--codex-context-dir", str(native), "--decision-output-root", str(tmp_path / "captures"), "--turn-instance-id", "scope-turn") + first = reference_metadata_oracle(first) page = read_context_capture(argparse.Namespace(capture_file=first["recovery"]["path"], capture_sha256=first["recovery"]["sha256"], offset=0, page_bytes=64000, max_page_output_bytes=64000, format="json")) parts = [] @@ -98,6 +100,26 @@ def cli(*args): # Use the shared exact selector, without a specialized outcome reader. source_index = next(i for i, row in enumerate(presentation["interaction_contract"]["agent_channel"]["work_context"]["sources"]) if row.get("source") == "goal_acceptance") + prefix = f"/interaction_contract/agent_channel/work_context/sources/{source_index}/content/goal_acceptance_contract/outcome_requirements/outcomes" + # The same public reader works without native/session flags. Its starting + # selection adopts the existing owner's suggestions, not an ownership rule. + native_before = (native / "session.json").read_bytes() + initial_parts, offset, selection = [], 0, [] + while True: + initial = cli("quota", "context-read", "--capture-file", first["recovery"]["path"], + "--capture-sha256", first["recovery"]["sha256"], "--initial-read", "--offset", str(offset), + "--page-bytes", "16000", "--max-page-output-bytes", "20000", *selection) + initial_parts.append(initial["content"]) + assert initial["sha256"] == first["recovery"]["sha256"] + if initial["eof"]: + break + offset = initial["next_offset"] + selection = ["--selection-sha256", initial["selection_sha256"]] + values = {row["pointer"]: row["value"] for row in json.loads("".join(initial_parts))["selections"]} + assert values[prefix + "/0"] == observed["goal_acceptance_contract"]["outcome_requirements"]["outcomes"][0] + assert values[prefix + "/1/id"] == "peer" + assert "peer exact requirement 11" not in "".join(initial_parts) + assert (native / "session.json").read_bytes() == native_before pointer = f"/interaction_contract/agent_channel/work_context/sources/{source_index}/content/goal_acceptance_contract/outcome_requirements/outcomes/1" detail = cli("quota", "context-read", "--capture-file", first["recovery"]["path"], "--capture-sha256", first["recovery"]["sha256"], "--select", pointer, From 7c80af20081a8b297bc3a31d8418462e6b9a96fe Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:38:27 +0530 Subject: [PATCH 2/5] docs(context): qualify scoped first-read delivery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../controller-executor-unified-contract-v0.md | 9 +++++++++ ...oller-executor-unified-contract-v0.zh-CN.md | 6 ++++++ docs/reference/required-work-context.md | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 668aa91b4d..7b3091deda 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1050,6 +1050,15 @@ identity use the existing projection-edit transport. This changes neither required-read obligations nor admission, settlement or the off path; it does not close C1/C2 adoption or establish model-outcome/full-cost benefit. +The existing initial reader also consumes complete acceptance navigation bound +to the captured Goal/Agent/Todo and source revisions. Suggested outcome bodies +reach the starting read, including peer dependencies; other outcomes remain +addressable. Missing or mismatched navigation keeps the prior detail route. +This reuses the relationship owner rather than adding an ownership/prose scope +rule. It changes neither activation defaults nor saved-view bytes or Delta; +matched initial-plus-required-detail traffic is a separate component comparison, +not the original quota outcome/full-cost qualification or an unread ledger. + C1's inventory is part of its usable replacement, not a standalone architecture framework. C3/C4/C5 can proceed independently once C1's contract is stable; C7 can prepare measurements before C6 but cannot claim two-session outcomes. diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 89e87e0578..31c24a3b6e 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -792,6 +792,12 @@ backoff 继续归当前 scheduler owner。本文不引入模型空轮询或新 本改动不改变必读义务、准入、结算或关闭路径,也不代表 C1/C2 采用资格或 模型结果与完整成本收益已经验收。 +现有首读还消费与捕获的 Goal/Agent/Todo 及来源版本绑定的完整 acceptance 导航, +将建议成果正文直接放入起始读取,包括 peer 依赖;其他成果仍可按地址展开。导航缺失 +或错配时保留原详情路线。复用既有关系 owner,不新增按归属/正文猜 scope 的规则; +不改变启用默认值、落盘视图字节或 Delta。相同首读加必需详情的流量只作独立组件对照, +不代表原 quota 成果/完整成本验收,也不交付未读台账。 + C1 盘点是可用替换的一部分,不单独建设架构框架。C1 契约稳定后,C3/C4/C5 可以独立 推进;C7 可以提前准备测量,但不能提前宣称双会话成果。实现 PR 列明替换的调用方、剩余 缺口、实际测试与回退。完整语言迁移或无关 manager 重写不是前置条件;新抽象能编译 diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index aea9943c75..b8cad984e4 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -177,6 +177,17 @@ evidence and the complete operation/settlement contract before depending on them The original SHA-bound full recovery remains available; a selected page's EOF does not prove complete source delivery, model retention or fresh authority. +When the existing acceptance owner supplies complete `outcome_context` +navigation matching this capture's Goal, Agent, selected Todo, outcome revision +and provider revision, the initial read expands its suggested outcomes in full +as present in the required view, including cross-Agent dependencies and unknown +outcome fields. Other outcomes retain their existing addresses; this is reading +guidance, not proof of independence or permission to skip applicable criteria. +Incomplete, mismatched, ambiguous or unavailable navigation keeps the existing +index/detail route. Folded supporting material still has its original read +obligations. The generic reader needs no Session Hook for this selection; +activation defaults, saved files, Delta and full-read receipts are unchanged. + 中文:已启用原生传输的完整引用可给出 `first_read_command`,合并当前准入、停止条件、 任务和完整 Goal/Todo 正文,以及来源状态、版本与读取入口;历史和可选格式说明仍在 同一捕获中按需展开。它是起始选读,不代表已读全部必要来源;依赖具体验收、证据或 @@ -187,6 +198,13 @@ does not prove complete source delivery, model retention or fresh authority. 说明与模板,使用前须读原附件;未知计划保留全文。首读分页不重复正文中的指针, 选读摘要仍阻止将它记作完整输入。 +既有 acceptance owner 给出的完整 `outcome_context` 导航,与当前 capture 的 Goal、 +Agent、所选 Todo、成果版本及 provider revision 一致时,首读完整展开其建议成果在 +必读视图中的正文,包括跨 Agent 依赖和未知成果字段;其他成果仍保留原地址。这是读取 +建议,不证明相互独立,也不允许省略适用要求。导航不完整、错配、歧义或不可用时保留 +原索引/详情路线;折叠的支持材料仍遵守原读取义务。此选读使用无需 Session Hook 的 +通用 reader,不改变启用默认值、落盘文件、Delta 或完整读取回执。 + Checkpoint Host preflight uses the hidden `--defer-context-presentation` option with an explicitly enabled native JSON `should-run` capture. It collects the same complete Goal source for admitted work-context reads and writes the ordinary From b93480d7a287f9e296d68e5925c8e2c095fdf2d0 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:12:32 +0530 Subject: [PATCH 3/5] fix(context): retain decision fields with the Turn owner Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/controller-executor-unified-contract-v0.md | 3 +++ .../rfcs/controller-executor-unified-contract-v0.zh-CN.md | 2 ++ loopx/control_plane/quota/progressive_context.ts | 8 ++------ loopx/control_plane/quota/turn_envelope.ts | 5 ++++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 336959b1d3..f892c0aa09 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1227,6 +1227,9 @@ This reuses the relationship owner rather than adding an ownership/prose scope rule. It changes neither activation defaults nor saved-view bytes or Delta; matched initial-plus-required-detail traffic is a separate component comparison, not the original quota outcome/full-cost qualification or an unread ledger. +The TurnEnvelope owner retains the top-level decision-field vocabulary and +supplies it to Progressive selection. Moving the selector does not duplicate +that vocabulary across owners or relax its semantic module budget. C1's inventory is part of its usable replacement, not a standalone architecture framework. C3/C4/C5 can proceed independently once C1's contract is stable; diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 46b8b87ca0..610c5dd2ed 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -926,6 +926,8 @@ backoff 继续归当前 scheduler owner。本文不引入模型空轮询或新 或错配时保留原详情路线。复用既有关系 owner,不新增按归属/正文猜 scope 的规则; 不改变启用默认值、落盘视图字节或 Delta。相同首读加必需详情的流量只作独立组件对照, 不代表原 quota 成果/完整成本验收,也不交付未读台账。 +顶层 decision 字段清单保留在 TurnEnvelope owner,由其传给 Progressive 选读;迁移 +selector 不跨 owner 重复声明该词汇,也不放宽语义模块预算。 C1 盘点是可用替换的一部分,不单独建设架构框架。C1 契约稳定后,C3/C4/C5 可以独立 推进;C7 可以提前准备测量,但不能提前宣称双会话成果。实现 PR 列明替换的调用方、剩余 diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index 5c674e27b4..5cadc85043 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -194,7 +194,7 @@ export function prepareProgressiveContextDelivery(request: JsonObject, legacyFie return contextProjectionChanges(source, {context: required.context, delivery, projected: required.projected}, sourceValues); } -export function initialContextReadPointers(context: JsonObject): string[] { +export function initialContextReadPointers(context: JsonObject, decisionFields: readonly string[]): string[] { const object = (value: unknown): JsonObject | undefined => value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : undefined; const interaction = object(context.interaction_contract); @@ -209,11 +209,7 @@ export function initialContextReadPointers(context: JsonObject): string[] { } pointers.push("/" + path.map(key => String(key).replaceAll("~", "~0").replaceAll("/", "~1")).join("/")); }; - for (const field of ["ok", "schema_version", "goal_id", "agent_id", "should_run", "effective_action", - "normal_delivery_allowed", "recovery_delivery_allowed", "self_repair_allowed", "capability_repair_allowed", "workspace_repair_allowed", - "safe_bypass_allowed", "safe_bypass_kind", "blocked_action_scope", "reason", - "execution_obligation", "goal_boundary", "heartbeat_receipt", "work_lane_contract", "selected_todo"]) - include(field); + for (const field of decisionFields) include(field); include("agent_lane_next_action", "next_step"); include("agent_lane_next_action", "next_action_basis"); include("interaction_contract", "user_channel"); diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 8677466dc6..b3df259152 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1411,7 +1411,10 @@ export function turnEnvelopeRoute(envelope: JsonObject, historicalText?: JsonObj function capturedDecisionSelection(request: JsonObject): JsonObject { const initial = request.initial_read === true || request.list_fields === true && Array.isArray(request.select) && request.select.length === 1 && request.select[0] === ""; - return capturedContextSelection(request, initial ? initialContextReadPointers(requireJsonObject(request.context, "captured context")) : []); + return capturedContextSelection(request, initial ? initialContextReadPointers(requireJsonObject(request.context, "captured context"), ["ok", "schema_version", "goal_id", "agent_id", "should_run", "effective_action", + "normal_delivery_allowed", "recovery_delivery_allowed", "self_repair_allowed", "capability_repair_allowed", "workspace_repair_allowed", + "safe_bypass_allowed", "safe_bypass_kind", "blocked_action_scope", "reason", + "execution_obligation", "goal_boundary", "heartbeat_receipt", "work_lane_contract", "selected_todo"]) : []); } export function evaluateTurnEnvelope(value: unknown): JsonObject { From daf5804d79f89cbec2435d9167528ca3904f0de6 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:24:56 +0530 Subject: [PATCH 4/5] feat(context): expose current supporting material without retention claims Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 11 ++ ...ller-executor-unified-contract-v0.zh-CN.md | 7 + docs/reference/required-work-context.md | 32 +++++ loopx/control_plane/quota/context_delivery.ts | 6 +- .../quota/progressive_context.ts | 19 ++- loopx/control_plane/quota/turn_envelope.ts | 1 + .../test_context_unread_reachability.py | 128 ++++++++++++++++++ 7 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 tests/control_plane/test_context_unread_reachability.py diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index e030c16ef0..f868899667 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1219,6 +1219,17 @@ identity use the existing projection-edit transport. This changes neither required-read obligations nor admission, settlement or the off path; it does not close C1/C2 adoption or establish model-outcome/full-cost benefit. +Native Progressive supporting-material discovery now has a bounded precursor: +an unconfirmed index binds exact deferred-field pointers to the current complete +projection. Initial reads and file-backed warm deltas expose the complete index, +without requiring an old packet or model retention. Selected reads do not clear +its conservative coverage status or create a second ledger. Current-source CLI +reads qualify transport into a synthetic next artifact, including changed and +reordered material and unavailable predecessors; they do not establish natural +model adoption. All-host defaults, other omitted material, trusted Host coverage, +writer/reader cutover and §9's outcome/full-cost comparison remain open. Index +overhead participates in the existing full-versus-folded byte guard. + C1's inventory is part of its usable replacement, not a standalone architecture framework. C3/C4/C5 can proceed independently once C1's contract is stable; C7 can prepare measurements before C6 but cannot claim two-session outcomes. diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index c27782448a..f55426af06 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -921,6 +921,13 @@ backoff 继续归当前 scheduler owner。本文不引入模型空轮询或新 本改动不改变必读义务、准入、结算或关闭路径,也不代表 C1/C2 采用资格或 模型结果与完整成本收益已经验收。 +原生 Progressive 支持材料发现增加有界前置实现:保守未确认索引将延迟字段的精确 +指针绑定当前完整 projection,首读和文件 warm delta 均完整呈现,不依赖旧包或模型 +retention。选读不会清除覆盖未知状态或新增第二账本。当前源 CLI 读取覆盖变化、重排 +及前序不可用,并进入合成后继产物;这只资格化运输,不证明自然模型采用。跨宿主默认、 +其他省略材料、可信 Host 覆盖、writer/reader 切换及第9节成果/完整成本对照仍开放。 +索引开销计入原完整/折叠字节守卫。 + C1 盘点是可用替换的一部分,不单独建设架构框架。C1 契约稳定后,C3/C4/C5 可以独立 推进;C7 可以提前准备测量,但不能提前宣称双会话成果。实现 PR 列明替换的调用方、剩余 缺口、实际测试与回退。完整语言迁移或无关 manager 重写不是前置条件;新抽象能编译 diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index f48a530000..b37574f883 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -216,6 +216,38 @@ does not prove complete source delivery, model retention or fresh authority. 回传入队不等于完成。待对端结果、计数、遗漏和不可用观测保留原来源语义。 完整请求按需读取,评估、执行或回传前仍须读其全文;首读不消费结果,也不授予权限。 +For supporting fields folded by the existing native Progressive owner, +`context_details.unread_context_index` contains the current complete +`projection.json` source path/digest and exact `pointers`. The initial reader +shows that index; a file-backed warm delta repeats it in full even when its +pointers are unchanged. Each pointer uses the ordinary `context-read` +`--capture-file`, `--capture-sha256` and `--select` route, independently of any +previous packet, retained model input or patch chain. These addresses belong to +the complete source, not the derived `required-context.json` recovery view. + +`coverage_confirmed=false` conservatively reports no trusted model-visible +coverage witness. A previous select, reader EOF or file receipt does not clear +an entry or create `confirmed_read_sections`/`consumed_pointers` state. +`read_before_dependent_work` preserves the original obligation to read material +when the decision or verification needs it; it is not an automatic full-read or +execution gate. Updated/reordered material binds to its current addresses; +removed material does not retain a predecessor route. The actual index and +guidance count toward the existing byte guard, including fallback to the +complete file when folding is not smaller. In-view deduplication alone adds no +index. This bounded native slice changes no activation defaults or off path; +cross-host rollout and trusted coverage observation remain C1/C2 work. + +中文:现有原生 Progressive owner 折叠的支持字段,在 +`context_details.unread_context_index` 中列出当前完整 `projection.json` 的路径、 +摘要和精确 `pointers`。首读呈现该索引;文件 warm delta 即使指针未变也完整重发。 +各指针通过普通 `context-read --capture-file ... --capture-sha256 ... --select ...` +独立读取,不依赖旧包、模型保留或补丁链;它们属于完整源,不属于派生 required view。 +`coverage_confirmed=false` 表示没有可信模型可见覆盖见证,选读、EOF或文件回执不会 +清除条目,也不新建已读账本。`read_before_dependent_work` 保留原先按决策/验证需要 +读取的义务,不成为自动全读或执行门。变化/重排绑定当前地址,移除材料不沿用前序入口。 +索引和指导计入原字节守卫,不更小时回退完整文件;仅视图内去重不新增索引。本原生切片 +不改启用默认值或 off 路径,跨宿主推广和可信覆盖观察仍属 C1/C2 未完成工作。 + Checkpoint Host preflight uses the hidden `--defer-context-presentation` option with an explicitly enabled native JSON `should-run` capture. It collects the same complete Goal source for admitted work-context reads and writes the ordinary diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 2d77c1f044..7853f79249 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -588,7 +588,7 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r /** Shared encoding within one presentation transaction; callers publish once * against their original source so numeric facts never round-trip through JS. */ export function encodeContextDelivery(request: JsonObject, legacyQuotaFields: readonly string[], - values: ProjectionSourceValues): JsonObject { + values: ProjectionSourceValues, fileNavigation: Path[] = []): JsonObject { const context = requireJsonObject(request.context, "current projection"); const identity = contextSourceIdentity(request, request.source_number_tokens); const surface = surfaceOf(request.surface ?? "work_context"), target = targetBinding(request, context, surface); @@ -612,7 +612,9 @@ export function encodeContextDelivery(request: JsonObject, legacyQuotaFields: re if (!Number.isSafeInteger(depth) || Number(depth) < 0 || Number(depth) >= MAX_ANCESTRY) return fullResult("full_refresh_due"); const before = freshPaths(prior, surface, legacyQuotaFields, binding), after = freshPaths(context, surface, legacyQuotaFields, target); if (!fileBacked && (!before.valid || !after.valid)) return fullResult("source_not_reusable"); - const changes = changeTree(prior, context, fileBacked ? [] : after.paths, fileBacked, + // The presentation owner may require navigation without opening the base. + // This does not turn file continuity into confirmed model delivery. + const changes = changeTree(prior, context, fileBacked ? fileNavigation : after.paths, fileBacked, {identity, before: ["baseline", "context"], after: ["context"], ...(request.source_edits === true ? {values} : {})}); const delta: JsonObject = {schema_version: VERSION, mode: "delta", target, base: binding, diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index f2d6a8bf7d..01f6065ccb 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -9,6 +9,8 @@ import {quotaSummaryReferences, contextSourceIdentity, contextProjectionChanges, import {encodeContextDelivery} from "./context_delivery.ts"; type Path = (string | number)[]; +const pointer = (path: Path): string => "/" + path.map(key => + String(key).replaceAll("~", "~0").replaceAll("/", "~1")).join("/"); const at = (root: unknown, path: Path): unknown => path.reduce((row, key) => Array.isArray(row) ? row[Number(key)] : jsonObject(row)?.[key], root); const known = (row: JsonObject, fields: string[]): boolean => Object.keys(row).every(key => fields.includes(key)); @@ -138,8 +140,7 @@ function requiredNativeContext(request: JsonObject, sourceValues: ProjectionSour for (const edit of deferred) { const path = edit.path as Path, owner = edit.parent as Path; delete parent(path)[path.at(-1)!]; - parent([...owner, String(edit.index_key)])[String(edit.index_key)] = "/" + owner.map(key => - String(key).replaceAll("~", "~0").replaceAll("/", "~1")).join("/"); + parent([...owner, String(edit.index_key)])[String(edit.index_key)] = pointer(owner); removed.push(["context", ...path]); } for (const edit of instructions) { @@ -147,12 +148,21 @@ function requiredNativeContext(request: JsonObject, sourceValues: ProjectionSour parent(path)[path.at(-1)!] = edit.value; } if (deferred.length) required.context_details = {...details, + unread_context_index: { + source: {path: details.path, sha256: details.sha256}, + coverage_confirmed: false, + read_before_dependent_work: true, + pointers: [...new Set(deferred.map(edit => pointer(edit.path as Path)))], + }, instruction: "This view retains complete requirements and current review/check states. " + "Source history, review reasoning and evidence references are supporting material. " + "Read them from this same immutable capture when a decision, verification or review depends on them. " + "For source history use supporting_material_pointer plus /sources. For review detail append " + "/requirements//agent_review/note or /requirements//agent_review/evidence_refs instead. " + "Repeat --select to batch needed fields without replaying the outcome's requirements. " + + "unread_context_index lists unconfirmed supporting fields in the current complete source. " + + "Use its source path and sha256 with quota context-read --select for each exact pointer. " + + "A prior selected read, EOF or saved file does not confirm model-visible coverage or retention. " + "Detail reads never establish a full baseline. " + "Uncertain or missing necessary facts must be recovered before dependent action; Agent review is not independent acceptance."}; } @@ -189,7 +199,10 @@ export function prepareProgressiveContextDelivery(request: JsonObject, legacyFie const required = requiredNativeContext(request, sourceValues); const codec = nativeContextContinuity({...request, context: required.context, ...(request.source_number_tokens === undefined ? {} : {source_number_tokens: required.tokens})}); - const delivery = encodeContextDelivery(codec, legacyFields, sourceValues); + const details = jsonObject(required.context.context_details); + const navigation: Path[] = details && Object.hasOwn(details, "unread_context_index") + ? [["context_details", "unread_context_index"]] : []; + const delivery = encodeContextDelivery(codec, legacyFields, sourceValues, navigation); const {operation: _operation, source_edits: _edits, source_number_tokens: _numbers, ...source} = request; return contextProjectionChanges(source, {context: required.context, delivery, projected: required.projected}, sourceValues); } diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 680e182cab..15c72c58bc 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1426,6 +1426,7 @@ function initialReadPointers(context: JsonObject): string[] { "safe_bypass_allowed", "safe_bypass_kind", "blocked_action_scope", "reason", "execution_obligation", "goal_boundary", "heartbeat_receipt", "work_lane_contract", "selected_todo"]) include(field); + include("context_details", "unread_context_index"); include("agent_lane_next_action", "next_step"); include("agent_lane_next_action", "next_action_basis"); include("interaction_contract", "user_channel"); diff --git a/tests/control_plane/test_context_unread_reachability.py b/tests/control_plane/test_context_unread_reachability.py new file mode 100644 index 0000000000..879ae461d2 --- /dev/null +++ b/tests/control_plane/test_context_unread_reachability.py @@ -0,0 +1,128 @@ +"""Current-capture detail discovery stays separate from delivery and retention.""" +import argparse +import copy +import json +from pathlib import Path +import shlex + +import pytest + +from loopx.cli_commands.quota_capture import read_context_capture +from loopx.control_plane.turn_driver import codex_context as context +from test_codex_context_lifecycle import setup, project +from test_progressive_native_context import payload, outcome +from test_quota_settlement_cli import _run_cli + + +@pytest.mark.parametrize("previous_detail_read", [False, True]) +@pytest.mark.parametrize("change", ["unchanged", "updated", "reordered"]) +def test_unconfirmed_material_reaches_next_turn_work_from_current_capture( + tmp_path, monkeypatch, previous_detail_read, change, +): + from loopx.cli_commands.quota_capture import native_context_capture_view + + setup(tmp_path, monkeypatch) + original = payload() + original["interaction_contract"] = {"schema_version": "loopx_interaction_contract_v0", + "agent_channel": {"work_context": {"sources": []}}} + # Necessary inline text makes a file delta useful independently of the + # heavy supporting material whose delivery has never been confirmed. + outcome(original)["requirements"][0]["description"] = "Keep this whole acceptance clause. " * 300 + outcome(original)["requirements"][0]["agent_review"]["note"] += "Tail constraint: keep the report local." + peer = copy.deepcopy(outcome(original)) + peer["id"] = "peer" + peer["owner_agent_id"] = "other-agent" + original["planning_horizon"]["core_goal"]["acceptance_contract"]["outcome_requirements"]["outcomes"].append(peer) + _, first = project(tmp_path, payload=original) + first_details = first["context"]["context_details"] + first_index = first_details["unread_context_index"] + assert first_index["coverage_confirmed"] is False + assert first_index["source"] == {key: first_details[key] for key in ("path", "sha256")} + registry, runtime = tmp_path / "no-registry.json", tmp_path / "no-runtime" + navigation = native_context_capture_view(first, Path(first["recovery"]["path"]).parent, + context_directory=str(tmp_path)) + argv = shlex.split(navigation["first_read_command"]) + code, initial = _run_cli(registry, runtime, *argv[argv.index("quota"):], cwd=tmp_path) + assert code == 0 and initial["eof"], initial + shown = {row["pointer"]: row["value"] for row in json.loads(initial["content"])["selections"]} + assert shown["/context_details/unread_context_index"] == first_index + + def read_detail(source, pointer): + words = ["quota", "context-read", "--capture-file", source["path"], + "--capture-sha256", source["sha256"], "--select", pointer, + "--page-bytes", "1200", "--max-page-output-bytes", "2600"] + body, offset = [], 0 + while True: + code, page = _run_cli(registry, runtime, *words, cwd=tmp_path) + assert code == 0 and page["ok"] and page["offset"] == offset, page + body.append(page["content"]) + if page["eof"]: + return json.loads("".join(body))["selections"][0]["value"] + offset = page["next_offset"] + argv = shlex.split(page["next_command"]) + words = argv[argv.index("quota"):] + + if previous_detail_read: + previous_note = next(pointer for pointer in first_index["pointers"] + if pointer.endswith("/outcomes/0/requirements/0/agent_review/note")) + assert read_detail(first_index["source"], previous_note).endswith("keep the report local.") + current = copy.deepcopy(original) + if change == "updated": + outcome(current)["requirements"][0]["agent_review"]["note"] += " Updated constraint: include the contrary result." + if change == "reordered": + current["planning_horizon"]["core_goal"]["acceptance_contract"]["outcome_requirements"]["outcomes"].reverse() + _, warm = project(tmp_path, "next-turn", payload=current) + assert warm["mode"] == "delta" + required = json.loads(Path(warm["recovery"]["path"]).read_bytes()) + index = required["context_details"]["unread_context_index"] + assert index["coverage_confirmed"] is False + assert Path(index["source"]["path"]).name == "projection.json" + assert index["source"]["path"] != first_index["source"]["path"] + + def literal_index(value): + if isinstance(value, dict): + found = value.get("unread_context_index") + if found == index or isinstance(found, dict) and found.get("value") == index: + return True + return any(literal_index(child) for child in value.values()) + return isinstance(value, list) and any(literal_index(child) for child in value) + + assert literal_index(warm["changes"]), "Warm discovery must not require an inherited index." + rows = current["planning_horizon"]["core_goal"]["acceptance_contract"]["outcome_requirements"]["outcomes"] + position = next(i for i, row in enumerate(rows) if row["id"] == "outcome") + pointer = (f"/planning_horizon/core_goal/acceptance_contract/outcome_requirements/outcomes/{position}" + "/requirements/0/agent_review/note") + assert pointer in index["pointers"] + peer_position = next(i for i, row in enumerate(rows) if row["id"] == "peer") + assert (f"/planning_horizon/core_goal/acceptance_contract/outcome_requirements/outcomes/{peer_position}/sources" + in index["pointers"]) + # No previous packet, retained model input or patch chain is available to + # this no-hook reader. Its actual next artifact must use the recovered tail. + Path(first["recovery"]["path"]).unlink() + Path(first_details["path"]).unlink() + material = read_detail(index["source"], pointer) + artifact = tmp_path / "next-turn-report.txt" + artifact.write_text(material) + assert artifact.read_text() == rows[position]["requirements"][0]["agent_review"]["note"] + assert "Tail constraint: keep the report local." in artifact.read_text() + assert not {"confirmed_read_sections", "consumed_pointers"} & context._state(tmp_path).keys() + assert not registry.exists() and not runtime.exists() + + +def test_removed_material_does_not_keep_a_predecessor_discovery_route(tmp_path, monkeypatch): + setup(tmp_path, monkeypatch) + original = payload() + _, first = project(tmp_path, payload=original) + assert first["context"]["context_details"]["unread_context_index"]["pointers"] + current = copy.deepcopy(original) + outcome(current)["sources"] = [] + outcome(current)["requirements"][0].pop("agent_review") + _, next_turn = project(tmp_path, "removed", payload=current) + recovery = next_turn["recovery"] + assert json.loads(Path(recovery["path"]).read_bytes()) == current + missing = read_context_capture(argparse.Namespace(capture_file=recovery["path"], + capture_sha256=recovery["sha256"], select=["/context_details/unread_context_index"], + offset=0, page_bytes=12000, max_page_output_bytes=18000, format="json")) + assert missing["ok"] is False + + From 5ac65c3cf150f1419f5c5775cd930c511619df9b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:39:37 +0530 Subject: [PATCH 5/5] style(tests): remove trailing blank line from unread regression Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_context_unread_reachability.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/control_plane/test_context_unread_reachability.py b/tests/control_plane/test_context_unread_reachability.py index 879ae461d2..b645662cb5 100644 --- a/tests/control_plane/test_context_unread_reachability.py +++ b/tests/control_plane/test_context_unread_reachability.py @@ -124,5 +124,3 @@ def test_removed_material_does_not_keep_a_predecessor_discovery_route(tmp_path, capture_sha256=recovery["sha256"], select=["/context_details/unread_context_index"], offset=0, page_bytes=12000, max_page_output_bytes=18000, format="json")) assert missing["ok"] is False - -