From 0edd9baac6281d64cd5e0575284bbf21f44c3cc9 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:00:25 +0800 Subject: [PATCH 1/2] Defer duplicate request rows to the required Manager Inbox read Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 22 ++++++---- .../quota/progressive_context.ts | 29 +++++++++++-- .../control_plane/test_context_first_read.py | 42 +++++++++++++++++++ 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 7ac6fe66e1..1d3d0b9c8d 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -213,12 +213,14 @@ corrections, retirement/expiry and empty replacements retain their provenance, revision and advisory authority. This captured pre-work view does not replace the preference owner's fresh exact-scope read before a later preference-dependent external action, and neither delivery nor a preference grants authority. -The existing bounded `unfinished_delegations` projection and `read_complete` -flag also remain visible. Adoption or a queued return does not establish -completion. Pending peer results, counts, omitted items and unavailable -observations retain their source semantics. Full request bodies stay on demand and must be -read before assessing, executing or returning a request; the initial view does -not consume a result or grant authority. +The initial read includes the bounded `unfinished_delegations` projection and +`read_complete` flag. For an admitted normal work Turn with a large, complete +agenda and a matching required Manager Inbox read, the initial view keeps its counts, +coverage, instructions and read command. The required read delivers the request +rows. The original rows remain available from the same capture. Adoption and +queued returns are intermediate states. Read the full current request before +assessing, executing or returning it. Use the existing scoped request and +consumption routes for those actions. It also exposes existing outcome material addresses and the current settlement plan's obligations, including unknown step fields. Only attachments explicitly marked `required=false` defer their guidance/templates; their routes stay visible @@ -366,9 +368,11 @@ full/delta 字节比较计入这些 reader 命令元数据;既有进程内引 首读也给出现有验收材料地址和当前结算义务。仅明确 `required=false` 的附件延后完整 说明与模板,使用前须读原附件;未知计划保留全文。首读分页不重复正文中的指针, 选读摘要仍阻止将它记作完整输入。 -已有的有界 `unfinished_delegations` 投影和 `read_complete` 标记也直接呈现;采纳和 -回传入队不等于完成。待对端结果、计数、遗漏和不可用观测保留原来源语义。 -完整请求按需读取,评估、执行或回传前仍须读其全文;首读不消费结果,也不授予权限。 +首读包含有界 `unfinished_delegations` 投影和 `read_complete` 标记。已准入的普通 +工作 Turn 若有完整的大型委派摘要,且必读列表中有同一条管家收件箱命令,首读保留 +计数、覆盖状态、说明和读取命令,逐条记录由必做的收件箱读取交付。原记录仍可从 +同一捕获回读。采纳和回传入队是中间状态,评估、执行或回传前须读当前请求全文, +并沿已有范围约束和消费入口办理。 既有 acceptance owner 给出的完整 `outcome_context` 导航,与当前 capture 的 Goal、 Agent、所选 Todo、成果版本及 provider revision 一致时,首读完整展开其建议成果在 diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index 1593589897..0fe5b3dec8 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -498,6 +498,25 @@ export function initialContextReadPointers(context: JsonObject, decisionFields: return [...opening, ...remaining]; } +/** A pending Manager Inbox read already delivers these scoped request rows. */ +function managerAgendaHeaderFields(value: unknown, requiredReads: unknown[]): string[] | undefined { + const {content, ...read} = jsonObject(value) ?? {}; + const source = jsonObject(content), agenda = jsonObject(source?.unfinished_delegations); + if (read.source !== "turn_start_capability_hook" || read.kind !== "operator_inbox" + || read.capability_id !== "manager-context" || read.hook_id !== "manager.context_inbox" + || read.ordering !== "before_work" || typeof read.command !== "string" || !read.command + || source?.ok !== true || source.read_complete !== false + || !agenda || agenda.read_command !== read.command || agenda.count_unit !== "request" + || agenda.scan_complete !== true || agenda.omitted_count !== 0 || agenda.unavailable_count !== 0 + || !Array.isArray(agenda.items) + || !known(agenda, ["count_unit", "items", "incoming_count", "outgoing_count", "omitted_count", + "scan_complete", "unavailable_count", "observed_at", "read_command", "instruction"]) + || !requiredReads.some(required => isDeepStrictEqual(required, read))) return undefined; + // Nine separate pointer wrappers cost more than a small inline agenda. + if (Buffer.byteLength(JSON.stringify(agenda.items), "utf8") < 2048) return undefined; + return Object.keys(agenda).filter(key => key !== "items"); +} + function initialContextReadGroups(context: JsonObject, decisionFields: readonly string[]): { opening: string[]; remaining: string[]; } { @@ -568,6 +587,9 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly const work = ["interaction_contract", "agent_channel", "work_context"]; include(...work, "instruction"); const channel = object(interaction.agent_channel); + const requiredInboxReads = context.effective_action === EffectiveAction.NORMAL_RUN && context.normal_delivery_allowed === true + && channel?.delivery_allowed === true && channel.must_attempt === true && Array.isArray(channel.required_reads) + ? channel.required_reads : []; const sources = object(channel?.work_context)?.sources; const sourceRows = Array.isArray(sources) ? sources.map((source, index) => ({source, index})) : []; const selectedTodo = object(context.selected_todo); @@ -629,10 +651,11 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly 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"); + const agendaPath = [...source, "content", "unfinished_delegations"]; + const agendaFields = managerAgendaHeaderFields(_source, requiredInboxReads); + if (agendaFields) for (const field of agendaFields) include(...agendaPath, field); + else include(...agendaPath); // The preference owner already supplies the complete current scoped view, // including corrections, retirement, provenance and advisory authority. // Its revision/instructions alone cannot fulfill that pre-work delivery. diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index 915e4ceaef..8110b7c72d 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -648,6 +648,48 @@ def test_initial_read_preserves_unfinished_requests_and_incomplete_observation(t assert detail["ok"] and "FULL-REQUEST-DETAIL-ON-DEMAND" in detail["content"] +@pytest.mark.parametrize("route", ["matching", "different", "missing", "incomplete", "unknown", "small", "held"]) +def test_initial_read_leaves_large_agenda_rows_to_the_matching_required_read(tmp_path, decision, route): + command = "loopx manager-inbox read --goal-id fixture --agent-id author" + read = {"source": "turn_start_capability_hook", "kind": "operator_inbox", + "capability_id": "manager-context", "hook_id": "manager.context_inbox", + "ordering": "before_work", "command": command} + agenda = {"count_unit": "request", "incoming_count": 2, "outgoing_count": 1, + "items": [{"request_id": f"request-{index}", "next_action": "保留原反馈与验收材料。" * (4 if route == "small" else 48)} + for index in range(3)], + "omitted_count": 0, "scan_complete": route != "incomplete", + "unavailable_count": int(route == "incomplete"), "read_command": command, + "instruction": "Read the full current request before acting."} + if route == "unknown": + agenda["future_gate"] = "Check the retained restriction." + decision.update(should_run=True, normal_delivery_allowed=True, effective_action="normal_run") + channel = decision["interaction_contract"]["agent_channel"] + channel.update(delivery_allowed=route != "held", must_attempt=True) + channel["work_context"]["sources"].append({**read, "content": { + "ok": True, "read_complete": False, "unfinished_delegations": agenda, + }}) + if route != "missing": + channel["required_reads"].append({**read, "command": command + " --agent-id other"} if route == "different" else read) + + view = native_context_capture_view(decision, tmp_path) + _, values = read_selection(view["first_read_command"]) + prefix = "/interaction_contract/agent_channel/work_context/sources/3/content" + assert values[prefix + "/read_complete"] is False + agenda_path = prefix + "/unfinished_delegations" + if route == "matching": + assert agenda_path not in values and agenda_path + "/items" not in values + for field in agenda.keys() - {"items"}: + assert values[f"{agenda_path}/{field}"] == agenda[field] + assert read in values["/interaction_contract/agent_channel/required_reads"] + else: + assert values[agenda_path] == agenda + detail = read_context_capture(Namespace(capture_file=view["recovery"]["path"], + capture_sha256=view["recovery"]["sha256"], format="json", select=[agenda_path], + offset=0, page_bytes=64000, max_page_output_bytes=64000)) + assert detail["ok"] and detail["eof"] + assert json.loads(detail["content"])["selections"][0]["value"] == agenda + + def test_unknown_settlement_plan_is_not_compacted(tmp_path, decision): plan = {"schema_version": "future", "new_stop": "Stop before every new effect."} decision["interaction_contract"]["cli_channel"]["settlement_plan"] = plan From 59eb26851dbf609649fb56b8faff58400724bb4e Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:04:37 +0800 Subject: [PATCH 2/2] Keep the initial agenda to counts and scan status Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 9 +++++---- .../quota/progressive_context.ts | 20 +++++++++++-------- .../control_plane/test_context_first_read.py | 18 ++++++++++------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 1d3d0b9c8d..664b65680d 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -215,9 +215,10 @@ preference owner's fresh exact-scope read before a later preference-dependent external action, and neither delivery nor a preference grants authority. The initial read includes the bounded `unfinished_delegations` projection and `read_complete` flag. For an admitted normal work Turn with a large, complete -agenda and a matching required Manager Inbox read, the initial view keeps its counts, -coverage, instructions and read command. The required read delivers the request -rows. The original rows remain available from the same capture. Adoption and +agenda and a matching required Manager Inbox read, the initial view keeps the +incoming and outgoing request counts, scan status and required command. The +required read delivers the rows and their guidance. The original rows remain +available from the same capture. Adoption and queued returns are intermediate states. Read the full current request before assessing, executing or returning it. Use the existing scoped request and consumption routes for those actions. @@ -370,7 +371,7 @@ full/delta 字节比较计入这些 reader 命令元数据;既有进程内引 选读摘要仍阻止将它记作完整输入。 首读包含有界 `unfinished_delegations` 投影和 `read_complete` 标记。已准入的普通 工作 Turn 若有完整的大型委派摘要,且必读列表中有同一条管家收件箱命令,首读保留 -计数、覆盖状态、说明和读取命令,逐条记录由必做的收件箱读取交付。原记录仍可从 +收发请求计数、扫描状态和必读命令,逐条记录及其说明由必做的收件箱读取交付。原记录仍可从 同一捕获回读。采纳和回传入队是中间状态,评估、执行或回传前须读当前请求全文, 并沿已有范围约束和消费入口办理。 diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index 0fe5b3dec8..409670a2ab 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -499,7 +499,7 @@ export function initialContextReadPointers(context: JsonObject, decisionFields: } /** A pending Manager Inbox read already delivers these scoped request rows. */ -function managerAgendaHeaderFields(value: unknown, requiredReads: unknown[]): string[] | undefined { +function canDeferManagerAgenda(value: unknown, requiredReads: unknown[]): boolean { const {content, ...read} = jsonObject(value) ?? {}; const source = jsonObject(content), agenda = jsonObject(source?.unfinished_delegations); if (read.source !== "turn_start_capability_hook" || read.kind !== "operator_inbox" @@ -511,10 +511,14 @@ function managerAgendaHeaderFields(value: unknown, requiredReads: unknown[]): st || !Array.isArray(agenda.items) || !known(agenda, ["count_unit", "items", "incoming_count", "outgoing_count", "omitted_count", "scan_complete", "unavailable_count", "observed_at", "read_command", "instruction"]) - || !requiredReads.some(required => isDeepStrictEqual(required, read))) return undefined; - // Nine separate pointer wrappers cost more than a small inline agenda. - if (Buffer.byteLength(JSON.stringify(agenda.items), "utf8") < 2048) return undefined; - return Object.keys(agenda).filter(key => key !== "items"); + || !requiredReads.some(required => isDeepStrictEqual(required, read))) return false; + const {incoming_count: incoming, outgoing_count: outgoing, items} = agenda; + if (typeof incoming !== "number" || !Number.isSafeInteger(incoming) || incoming < 0 + || typeof outgoing !== "number" || !Number.isSafeInteger(outgoing) || outgoing < 0 + || incoming + outgoing !== items.length) return false; + // Keep small agendas inline. Three pointers retain the starting counts and + // scan status without repeating the required read's details or guidance. + return Buffer.byteLength(JSON.stringify(items), "utf8") >= 2048; } function initialContextReadGroups(context: JsonObject, decisionFields: readonly string[]): { @@ -653,9 +657,9 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly include(...source, "content", field); include(...source, "content", "read_complete"); const agendaPath = [...source, "content", "unfinished_delegations"]; - const agendaFields = managerAgendaHeaderFields(_source, requiredInboxReads); - if (agendaFields) for (const field of agendaFields) include(...agendaPath, field); - else include(...agendaPath); + if (canDeferManagerAgenda(_source, requiredInboxReads)) { + for (const field of ["incoming_count", "outgoing_count", "scan_complete"]) include(...agendaPath, field); + } else include(...agendaPath); // The preference owner already supplies the complete current scoped view, // including corrections, retirement, provenance and advisory authority. // Its revision/instructions alone cannot fulfill that pre-work delivery. diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index 8110b7c72d..3615bfb395 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -648,16 +648,18 @@ def test_initial_read_preserves_unfinished_requests_and_incomplete_observation(t assert detail["ok"] and "FULL-REQUEST-DETAIL-ON-DEMAND" in detail["content"] -@pytest.mark.parametrize("route", ["matching", "different", "missing", "incomplete", "unknown", "small", "held"]) +@pytest.mark.parametrize("route", [ + "matching", "different", "missing", "incomplete", "omitted", "unknown", "bad_counts", "small", "held", +]) def test_initial_read_leaves_large_agenda_rows_to_the_matching_required_read(tmp_path, decision, route): command = "loopx manager-inbox read --goal-id fixture --agent-id author" read = {"source": "turn_start_capability_hook", "kind": "operator_inbox", "capability_id": "manager-context", "hook_id": "manager.context_inbox", "ordering": "before_work", "command": command} - agenda = {"count_unit": "request", "incoming_count": 2, "outgoing_count": 1, - "items": [{"request_id": f"request-{index}", "next_action": "保留原反馈与验收材料。" * (4 if route == "small" else 48)} - for index in range(3)], - "omitted_count": 0, "scan_complete": route != "incomplete", + purpose = "保留原反馈与验收材料。" * (4 if route == "small" else 48) + agenda = {"count_unit": "request", "incoming_count": 3 if route == "bad_counts" else 2, "outgoing_count": 1, + "items": [{"request_id": f"request-{index}", "next_action": purpose} for index in range(3)], + "omitted_count": int(route == "omitted"), "scan_complete": route != "incomplete", "unavailable_count": int(route == "incomplete"), "read_command": command, "instruction": "Read the full current request before acting."} if route == "unknown": @@ -669,7 +671,8 @@ def test_initial_read_leaves_large_agenda_rows_to_the_matching_required_read(tmp "ok": True, "read_complete": False, "unfinished_delegations": agenda, }}) if route != "missing": - channel["required_reads"].append({**read, "command": command + " --agent-id other"} if route == "different" else read) + required = {**read, "command": command + " --agent-id other"} if route == "different" else read + channel["required_reads"].append(required) view = native_context_capture_view(decision, tmp_path) _, values = read_selection(view["first_read_command"]) @@ -678,8 +681,9 @@ def test_initial_read_leaves_large_agenda_rows_to_the_matching_required_read(tmp agenda_path = prefix + "/unfinished_delegations" if route == "matching": assert agenda_path not in values and agenda_path + "/items" not in values - for field in agenda.keys() - {"items"}: + for field in ("incoming_count", "outgoing_count", "scan_complete"): assert values[f"{agenda_path}/{field}"] == agenda[field] + assert agenda_path + "/instruction" not in values assert read in values["/interaction_contract/agent_channel/required_reads"] else: assert values[agenda_path] == agenda