diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index e030c16ef0..f892c0aa09 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -1219,6 +1219,18 @@ 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. +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; 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..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 @@ -921,6 +921,14 @@ backoff 继续归当前 scheduler owner。本文不引入模型空轮询或新 本改动不改变必读义务、准入、结算或关闭路径,也不代表 C1/C2 采用资格或 模型结果与完整成本收益已经验收。 +现有首读还消费与捕获的 Goal/Agent/Todo 及来源版本绑定的完整 acceptance 导航, +将建议成果正文直接放入起始读取,包括 peer 依赖;其他成果仍可按地址展开。导航缺失 +或错配时保留原详情路线。复用既有关系 owner,不新增按归属/正文猜 scope 的规则; +不改变启用默认值、落盘视图字节或 Delta。相同首读加必需详情的流量只作独立组件对照, +不代表原 quota 成果/完整成本验收,也不交付未读台账。 +顶层 decision 字段清单保留在 TurnEnvelope owner,由其传给 Progressive 选读;迁移 +selector 不跨 owner 重复声明该词汇,也不放宽语义模块预算。 + 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..a0a4f44a57 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -203,6 +203,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 正文,以及来源状态、版本与读取入口;历史和可选格式说明仍在 同一捕获中按需展开。它是起始选读,不代表已读全部必要来源;依赖具体验收、证据或 @@ -216,6 +227,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 diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index f2d6a8bf7d..5cadc85043 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, 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); + // 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 decisionFields) 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); + // The capability already bounds this agenda. Keep pending returns and + // incomplete observations visible without expanding full request bodies. + include(...source, "content", "read_complete"); + include(...source, "content", "unfinished_delegations"); + 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 680e182cab..b3df259152 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,103 +1406,15 @@ 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); - // The capability already bounds this agenda. Keep pending returns and - // incomplete observations visible without expanding full request bodies. - include(...source, "content", "read_complete"); - include(...source, "content", "unfinished_delegations"); - 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"), ["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 { diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index 93c25d528e..d457584425 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -179,6 +179,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,