From 3645d81b2d15a1871f17d5601631fe846664d0c3 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Sat, 12 Sep 2026 12:30:43 +0800 Subject: [PATCH] fix(turn): warn on envelope growth and gate static delegation guidance Signed-off-by: huangruiteng --- docs/reference/protocols/turn-envelope-v0.md | 58 ++++++- loopx/cli_commands/turn_rendering.py | 8 + loopx/control_plane/quota/turn_envelope.ts | 37 +--- .../quota/turn_envelope_budget.ts | 76 ++++++++ loopx/control_plane/turn_driver/driver.py | 5 +- .../turn_driver/loop_controller.py | 6 +- .../renderers/turn_envelope_markdown.py | 19 ++ skills/loopx-project/SKILL.md | 19 +- .../test_turn_envelope_budget_warning.py | 164 ++++++++++++++++++ tests/control_plane_ts/turn_envelope.test.ts | 16 ++ tests/test_loop_turn_loop_controller.py | 6 +- tests/test_loopx_turn_driver.py | 7 +- tests/test_turn_envelope.py | 4 +- 13 files changed, 363 insertions(+), 62 deletions(-) create mode 100644 loopx/control_plane/quota/turn_envelope_budget.ts create mode 100644 tests/control_plane/test_turn_envelope_budget_warning.py diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 04776d2313..403e088e8b 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -110,8 +110,56 @@ the default quota output. Large todo summaries, frontier diagnostics, readiness history, compatibility fields, and warning collections stay on the referenced full-decision/status -cold paths. The envelope has an 8 KiB JSON budget and reports its measured -source/envelope byte counts. +cold paths. The envelope has an **8 KiB compact UTF-8 JSON performance target**, +not an execution-admission limit. `compaction.envelope_utf8_bytes` measures the +final packet, including diagnostics. The historical `source_json_bytes` and +`envelope_json_bytes` fields still count Unicode code points for v0 compatibility; +do not use them as wire-byte measurements. + +### Budget warnings and allocation + +Oversize valid envelopes keep their normal Turn plan/controller route. They +report `compaction.within_budget=false` and a structured +`warning.code=turn_envelope_budget_exceeded`, with `excess_bytes`, additive +`section_bytes` and `over_target_sections`. JSON carries this through the Turn +plan and host request; Markdown plan/envelope output calls out the warning. +Schema, signatures, identity, permissions, receipt validation and execution +quota are still hard gates. This changes previous behavior for **all Turn hosts**: +packet growth alone no longer produces `contract_error` or stops a Turn loop. + +The TypeScript owner keeps review allocations totaling 8,192 bytes. These are +diagnostic targets, not permission to truncate fields or hard per-section caps: + +| Section | Target bytes | Included fields | +| --- | ---: | --- | +| action | 800 | action, user, required reads, replan packet, response plan | +| boundary | 2,000 | boundary and execution policy | +| writeback | 600 | validation/settlement commands and policy | +| scheduler | 600 | scheduler action and acknowledgement | +| contracts | 1,800 | contract capsule | +| context | 1,400 | capability context and task orchestration | +| transport | 992 | identity/metadata, signatures, cold-read commands, diagnostics | + +Counts include JSON property names, delimiters and UTF-8 text. Their sum equals +the measured final packet; dividing each by `envelope_utf8_bytes` gives its +share. Diagnostic detail is emitted only on overflow, not every normal Turn. +Use the existing `quota should-run --turn-envelope` or `turn plan` JSON output +to inspect the breakdown. Record a public-safe reproduction and compare each +section with the same fixture on the baseline before changing its owner. +First remove repeated presentation or move non-actionable detail to an existing +cold read. Never trim write scope, executable arguments, signatures or required +reads to silence a warning, and do not simply raise the target. The cold-read +commands remain; their redundant human-readable `contains` inventory is retired. + +Repository size/parity canaries remain blocking **delivery-time regression +checks**, independent of runtime warning semantics. Representative fixtures must +still fit the target. A warning is a performance investigation signal, not an +automatic Todo, new authority, or permission to spend an extra Turn. + +中文:TurnEnvelope 超出 8 KiB 后产生可分析的 warning,不再仅因大小中断合法 +Turn。按最终 UTF-8 字节数统计各部分占比,先压缩重复展示内容,再检查对应规则 +所属模块;不得截断权限、签名或执行指令,也不应单纯提高预算掩盖增长。 +身份、权限、签名和执行配额仍是硬门禁;仓库的体积与语义回归检查仍阻止交付。 Hot-path fields may use explicit references when the inline value would only repeat another authoritative field. In particular, @@ -136,9 +184,9 @@ blocked, and throttled decisions. Every case must preserve the canonical action signature, reconstruct `protocol_action_packet`, and remain within the 8 KiB budget. -The current matrix produces envelopes from 4,866 to 5,602 bytes, with 66.44% to -69.36% reduction from the full synthetic decision. This is sufficient to keep -the projection available as an opt-in host view. It is not sufficient to change +The matrix records exact measurements in validation rather than treating a +dated size range as the contract. This keeps the projection available as an +opt-in host view. It is not sufficient to change the default CLI response: default promotion still requires shadow parity from a real host integration, no consumer regression with the full decision available as a cold path, and explicit compatibility acceptance for the default-view diff --git a/loopx/cli_commands/turn_rendering.py b/loopx/cli_commands/turn_rendering.py index 36b8070256..b81ecbea4b 100644 --- a/loopx/cli_commands/turn_rendering.py +++ b/loopx/cli_commands/turn_rendering.py @@ -1,5 +1,9 @@ from __future__ import annotations +from ..presentation.renderers.turn_envelope_markdown import ( + turn_envelope_budget_warning_lines, +) + def render_loopx_turn_plan_markdown(payload: dict[str, object]) -> str: if not payload.get("ok"): @@ -7,6 +11,7 @@ def render_loopx_turn_plan_markdown(payload: dict[str, object]) -> str: return f"LoopX Turn plan failed: {error}" host = payload.get("host") if isinstance(payload.get("host"), dict) else {} route = payload.get("route") if isinstance(payload.get("route"), dict) else {} + envelope = payload.get("turn_envelope") return "\n".join( [ "# LoopX Turn Plan", @@ -15,6 +20,9 @@ def render_loopx_turn_plan_markdown(payload: dict[str, object]) -> str: f"- route: {route.get('kind')}", f"- would_invoke_host: {route.get('would_invoke_host')}", "- side_effects: none", + *turn_envelope_budget_warning_lines( + envelope if isinstance(envelope, dict) else {} + ), ] ) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index a580956951..6d5366c835 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -6,9 +6,10 @@ import { } from "../effect_program.ts"; import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { requireJsonObject } from "../runtime_decode.ts"; +import { measureTurnEnvelope, TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts"; +export { TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts"; export const TURN_ENVELOPE_SCHEMA_VERSION = "loopx_turn_envelope_v0"; -export const TURN_ENVELOPE_BUDGET_BYTES = 8_192; export const CONTRACT_CAPSULE_SCHEMA_VERSION = "loopx_contract_capsule_v0"; export const ACTION_SIGNATURE_SCHEMA_VERSION = "loopx_action_signature_v0"; export const ACTION_SIGNATURE_COVERAGE_V0 = "turn_envelope_action_dimensions_v0"; @@ -178,10 +179,6 @@ function comparePythonUnicode(left: string, right: string): number { return leftPoints.length - rightPoints.length; } -function compactJson(value: unknown): string { - return JSON.stringify(value); -} - function canonicalHash(value: unknown): string { return `sha256:${createHash("sha256").update(JSON.stringify(canonicalValue(value)), "utf8").digest("hex")}`; } @@ -722,14 +719,6 @@ function coldPath( : "rerun the typed quota_guard from the current host packet", todo_detail: `${prefix} --format json todo list --goal-id ${goalId}`, status_detail: `${prefix} --format json status --goal-id ${goalId}`, - contains: [ - "quota accounting detail", - "goal frontier and route diagnostics", - "full todo summaries", - "handoff and readiness diagnostics", - "promotion, archive, and projection warnings", - "scheduler runtime detail", - ], }; } @@ -774,27 +763,7 @@ export function buildTurnEnvelope(value: unknown): JsonObject { matches: JSON.stringify(sourceSignature) === JSON.stringify(envelopeSignature), source_decision_hash: canonicalHash(payload), }; - // Preserve the versioned v0 metric: the historical Python owner counted - // Unicode code points even though the public field is named *_json_bytes. - const sourceBytes = [...compactJson(payload)].length; - envelope.compaction = { - source_json_bytes: sourceBytes, - envelope_json_bytes: 0, - byte_reduction_ratio: 0, - budget_bytes: TURN_ENVELOPE_BUDGET_BYTES, - within_budget: true, - }; - for (let index = 0; index < 3; index += 1) { - const envelopeBytes = [...compactJson(envelope)].length; - envelope.compaction = { - ...object(envelope.compaction), - envelope_json_bytes: envelopeBytes, - byte_reduction_ratio: sourceBytes - ? Math.round((1 - envelopeBytes / sourceBytes) * 10_000) / 10_000 - : 0, - within_budget: envelopeBytes <= TURN_ENVELOPE_BUDGET_BYTES, - }; - } + measureTurnEnvelope(envelope, payload); return envelope; } diff --git a/loopx/control_plane/quota/turn_envelope_budget.ts b/loopx/control_plane/quota/turn_envelope_budget.ts new file mode 100644 index 0000000000..94fd60638a --- /dev/null +++ b/loopx/control_plane/quota/turn_envelope_budget.ts @@ -0,0 +1,76 @@ +/** Performance diagnostics, never Turn admission or execution authority. */ +import type { JsonObject } from "../effect_program.ts"; + +export const TURN_ENVELOPE_BUDGET_BYTES = 8_192; + +// Review allocations, not truncation limits. Preserve authority even on overflow. +export const TURN_ENVELOPE_SECTION_TARGETS = { + action: 800, boundary: 2_000, writeback: 600, scheduler: 600, + contracts: 1_800, context: 1_400, transport: 992, +} as const; +type Section = keyof typeof TURN_ENVELOPE_SECTION_TARGETS; +const SECTION_FIELDS: Record = { + action: "action", user: "action", required_reads: "action", + replan_action_packet: "action", response_plan: "action", + boundary: "boundary", execution_policy: "boundary", writeback: "writeback", + scheduler: "scheduler", contract_capsule: "contracts", + agent_context: "context", task_orchestration_contract: "context", +}; + +function sectionBytes(envelope: JsonObject): Record { + const sizes = Object.fromEntries( + Object.keys(TURN_ENVELOPE_SECTION_TARGETS).map((key) => [key, 0]), + ) as Record; + // Include property names, separators and braces; totals equal wire bytes. + sizes.transport = 1; + for (const [key, value] of Object.entries(envelope)) { + sizes[SECTION_FIELDS[key] ?? "transport"] += + Buffer.byteLength(JSON.stringify(key) + ":" + JSON.stringify(value), "utf8") + 1; + } + return sizes; +} + +export function measureTurnEnvelope(envelope: JsonObject, source: JsonObject): void { + // Keep v0 *_json_bytes code-point metrics for compatibility. New diagnostics + // and the performance target use actual compact JSON UTF-8 bytes. + const sourceChars = [...JSON.stringify(source)].length; + envelope.compaction = { + source_json_bytes: sourceChars, envelope_json_bytes: 0, + byte_reduction_ratio: 0, budget_bytes: TURN_ENVELOPE_BUDGET_BYTES, + within_budget: true, envelope_utf8_bytes: 0, + }; + // Measurements include their own serialized metadata. Recompute to a fixed + // point (decimal widths and the four-place ratio stabilize after a few passes). + const seen = new Set(); + let ratioLocked = false; + for (let pass = 0; pass < 16; pass += 1) { + const before = JSON.stringify(envelope); + // Rounding can alternate between e.g. 0.54 and 0.5401, changing its own + // width. Freeze that approximate ratio on a cycle; byte counts stay exact. + if (seen.has(before)) ratioLocked = true; + seen.add(before); + const chars = [...before].length; + const bytes = Buffer.byteLength(before, "utf8"); + const metric: JsonObject = { + source_json_bytes: sourceChars, envelope_json_bytes: chars, + byte_reduction_ratio: ratioLocked + ? (envelope.compaction as JsonObject).byte_reduction_ratio : sourceChars + ? Math.round((1 - chars / sourceChars) * 10_000) / 10_000 : 0, + budget_bytes: TURN_ENVELOPE_BUDGET_BYTES, + within_budget: bytes <= TURN_ENVELOPE_BUDGET_BYTES, + envelope_utf8_bytes: bytes, + }; + if (bytes > TURN_ENVELOPE_BUDGET_BYTES) { + const sections = sectionBytes(envelope); + metric.warning = { + code: "turn_envelope_budget_exceeded", severity: "warning", + excess_bytes: bytes - TURN_ENVELOPE_BUDGET_BYTES, + section_bytes: sections, + over_target_sections: (Object.keys(sections) as Section[]) + .filter((key) => sections[key] > TURN_ENVELOPE_SECTION_TARGETS[key]), + }; + } + envelope.compaction = metric; + if (JSON.stringify(envelope) === before) break; + } +} diff --git a/loopx/control_plane/turn_driver/driver.py b/loopx/control_plane/turn_driver/driver.py index 573808119b..db0a9fc3bb 100644 --- a/loopx/control_plane/turn_driver/driver.py +++ b/loopx/control_plane/turn_driver/driver.py @@ -80,9 +80,8 @@ def _typed_route(envelope: Mapping[str, Any]) -> LoopXTurnRoute: or source_hash != envelope_hash ): return LoopXTurnRoute.CONTRACT_ERROR - compaction = _mapping(envelope.get("compaction")) - if compaction.get("within_budget") is not True: - return LoopXTurnRoute.CONTRACT_ERROR + # Packet size is a performance warning, not execution authority. Keep the + # diagnostics in the envelope; schema/signature/lineage remain hard gates. action = _mapping(envelope.get("action")) user = _mapping(envelope.get("user")) diff --git a/loopx/control_plane/turn_driver/loop_controller.py b/loopx/control_plane/turn_driver/loop_controller.py index d82fb370a5..c209ecfde4 100644 --- a/loopx/control_plane/turn_driver/loop_controller.py +++ b/loopx/control_plane/turn_driver/loop_controller.py @@ -103,8 +103,8 @@ def _envelope_route(decision: Mapping[str, Any]) -> LoopXTurnRoute: """Return the shared typed route for a fresh quota/scheduler decision. Reuses the Turn plan driver's ``_typed_route`` contract, which requires a - matching action signature with non-empty equal hashes and an in-budget - compaction. A projected user action outranks delivery, so it is resolved + matching action signature with non-empty equal hashes. Compaction budget + warnings are diagnostic only. A projected user action outranks delivery, so it is resolved before the typed delivery route. Raises ``ValueError`` when the envelope fails the shared contract instead of accepting a forged or truncated decision. @@ -114,7 +114,7 @@ def _envelope_route(decision: Mapping[str, Any]) -> LoopXTurnRoute: if route is LoopXTurnRoute.CONTRACT_ERROR: raise ValueError( "quota decision failed the shared envelope contract " - "(schema, signature hashes, or compaction budget)" + "(schema or signature hashes)" ) user = _mapping(decision.get("user")) if user.get("action_required") is True: diff --git a/loopx/presentation/renderers/turn_envelope_markdown.py b/loopx/presentation/renderers/turn_envelope_markdown.py index 1501f65bb2..3cc1860bd0 100644 --- a/loopx/presentation/renderers/turn_envelope_markdown.py +++ b/loopx/presentation/renderers/turn_envelope_markdown.py @@ -3,6 +3,24 @@ from typing import Any +def turn_envelope_budget_warning_lines(payload: dict[str, Any]) -> list[str]: + compaction = payload.get("compaction") or {} + warning = compaction.get("warning") or {} + if warning.get("code") != "turn_envelope_budget_exceeded": + return [] + sections = warning.get("section_bytes") or {} + return [ + "- WARNING: TurnEnvelope exceeds its performance target by " + f"{warning.get('excess_bytes')} UTF-8 bytes; Turn routing is unchanged.", + "- section_bytes: " + + ", ".join(f"{key}={value}" for key, value in sections.items()), + "- Review over-target sections: " + + ", ".join(warning.get("over_target_sections") or []), + "- Compress duplicate presentation or move detail to cold reads; " + "do not truncate authority or raise the target to hide growth.", + ] + + def render_turn_envelope_markdown(payload: dict[str, Any]) -> str: action_value = payload.get("action") user_value = payload.get("user") @@ -29,5 +47,6 @@ def render_turn_envelope_markdown(payload: dict[str, Any]) -> str: f"- scheduler: `{scheduler.get('action')}`", f"- envelope_bytes: `{compaction.get('envelope_json_bytes')}`", f"- within_budget: `{compaction.get('within_budget')}`", + *turn_envelope_budget_warning_lines(payload), ] return "\n".join(lines) diff --git a/skills/loopx-project/SKILL.md b/skills/loopx-project/SKILL.md index a5b5f5c140..f33287918b 100644 --- a/skills/loopx-project/SKILL.md +++ b/skills/loopx-project/SKILL.md @@ -1064,14 +1064,17 @@ ids, or raw local evidence in public repo docs or examples. ## Capability Context And Child Models Read `interaction_contract.agent_context` at planning time (or -`turn_envelope.agent_context` in LoopX Turn). Enabled capabilities contribute -bounded guidance for the coordinator, including independent evidence questions, -the work to retain locally, and parent validation obligations. Consider useful -read-heavy delegation within a single Todo; do not manufacture persistent Todos -or duplicate research to trigger parallelism. Respect the existing admission -and authorization boundaries. - -When using native child tools outside LoopX Turn, read the same capability +`turn_envelope.agent_context` in LoopX Turn, resolving its detail reference when +compacted). If the host supplies neither, use the read-only +`loopx agent-context --goal-id --agent-id --phase before_plan`. +If context is absent, disabled, or the read fails, preserve the existing single +agent workflow: do not seek delegation splits or invoke child tools because of +this capability. Tool availability and installed skills do not activate it. +Only apply delegation guidance from a non-null, current-scope enabled context; +the capability provider owns that policy. Context never grants spawn authority. + +When enabled context and separate authorization allow native child tools +outside LoopX Turn, read the same capability context at each boundary, using the current registry, Goal and Agent: ```bash diff --git a/tests/control_plane/test_turn_envelope_budget_warning.py b/tests/control_plane/test_turn_envelope_budget_warning.py new file mode 100644 index 0000000000..f1557ea510 --- /dev/null +++ b/tests/control_plane/test_turn_envelope_budget_warning.py @@ -0,0 +1,164 @@ +"""Size diagnostics must not acquire authority over valid Turn execution.""" + +from copy import deepcopy +import json +import subprocess +import sys + +import pytest + +from loopx.cli_commands.turn_rendering import render_loopx_turn_plan_markdown +from loopx.control_plane.quota.turn_envelope import ( + build_turn_envelope, + quota_action_signature_document, + turn_envelope_action_signature_document, +) +from loopx.control_plane.turn_driver import build_loopx_turn_plan +from loopx.presentation.renderers.turn_envelope_markdown import ( + render_turn_envelope_markdown, +) +from loopx.workflow_skill_install import workflow_skill_install +from tests.control_plane.test_agent_context import POLICY, SCOPE, context +from tests.test_loopx_turn_driver import _write_live_fixture +from tests.test_turn_envelope import _full_decision + + +def _wire(value): + return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + + +@pytest.mark.parametrize("enabled", [False, True]) +@pytest.mark.parametrize("text", ["x", "界", "🚀"]) +def test_oversized_envelope_measures_all_sections_without_losing_authority( + enabled, text +): + source = _full_decision() + source["goal_boundary"]["write_scope"] = [ + f"public/{i}/" + text * (80 if text == "🚀" else 170) for i in range(16) + ] + source["goal_boundary"]["guards"] = [f"guard-{i}-" + "x" * 200 for i in range(8)] + if enabled: + source["interaction_contract"]["agent_context"] = context() + envelope = build_turn_envelope(source) + metric = envelope["compaction"] + assert metric["within_budget"] is False + assert metric["envelope_utf8_bytes"] == len(_wire(envelope)) + assert metric["envelope_json_bytes"] == len(_wire(envelope).decode("utf-8")) + warning = metric["warning"] + assert warning["severity"] == "warning" + assert warning["excess_bytes"] == len(_wire(envelope)) - 8192 + assert sum(warning["section_bytes"].values()) == len(_wire(envelope)) + assert "boundary" in warning["over_target_sections"] + assert quota_action_signature_document( + source + ) == turn_envelope_action_signature_document(envelope) + plan = build_loopx_turn_plan( + envelope, host="codex-cli", execution_mode="interactive-visible" + ) + assert plan["ok"] is True + assert plan["route"]["would_invoke_host"] is True + assert "WARNING" in render_loopx_turn_plan_markdown(plan) + assert "section_bytes" in render_turn_envelope_markdown(envelope) + invalid = deepcopy(envelope) + invalid["action_signature"]["matches"] = False + assert ( + build_loopx_turn_plan( + invalid, host="codex-cli", execution_mode="interactive-visible" + )["ok"] + is False + ) + + +def test_normal_envelope_retires_redundant_cold_path_inventory(): + envelope = build_turn_envelope(_full_decision()) + assert "contains" not in envelope["detail_ref"] + assert {"full_decision", "todo_detail", "status_detail"} == set( + envelope["detail_ref"] + ) + assert "warning" not in envelope["compaction"] + assert envelope["compaction"]["envelope_utf8_bytes"] == len(_wire(envelope)) + + +def test_context_high_water_keeps_legal_scope_and_recovers_headroom(): + source = _full_decision() + scopes = [f"public/{i:02}/" + "x" * 160 for i in range(16)] + source["goal_boundary"]["write_scope"] = scopes + baseline = build_turn_envelope(source) + assert baseline["compaction"]["within_budget"] is True + source["interaction_contract"]["agent_context"] = context() + envelope = build_turn_envelope(source) + assert envelope["agent_context"]["detail_ref"] + assert envelope["boundary"]["write_scope"] == scopes + assert len(_wire(envelope)) <= 8192 + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_real_turn_plan_cli_warns_without_state_writes(tmp_path, enabled): + project, runtime, registry = _write_live_fixture(tmp_path) + config = json.loads(registry.read_text()) + goal = config["goals"][0] + goal["coordination"]["write_scope"] = [ + f"public/{i}/" + "界" * 160 for i in range(16) + ] + goal["spawn_policy"] = {**POLICY, "spawn_allowed": enabled} + registry.write_text(json.dumps(config)) + before = {p: p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()} + command = [ + sys.executable, + "-m", + "loopx.cli", + "--registry", + str(registry), + "--runtime-root", + str(runtime), + "--format", + "json", + "turn", + "plan", + "--goal-id", + "loopx-turn-fixture", + "--agent-id", + "codex-fixture", + "--scan-root", + str(project), + ] + result = subprocess.run(command, capture_output=True, text=True, timeout=120) + assert result.returncode == 0, result.stdout + result.stderr + plan = json.loads(result.stdout) + assert plan["ok"] is True + assert plan["route"]["would_invoke_host"] is True + envelope = plan["turn_envelope"] + assert bool(envelope.get("agent_context")) is enabled + assert envelope["compaction"]["warning"]["code"] == "turn_envelope_budget_exceeded" + assert envelope["compaction"]["envelope_utf8_bytes"] == len(_wire(envelope)) + assert before == {p: p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()} + + +def test_installed_skill_defers_delegation_policy_to_enabled_provider(tmp_path): + result = workflow_skill_install(skills_dir=tmp_path / "skills", execute=True) + assert result["ok"] is True + installed = (tmp_path / "skills/loopx-project/SKILL.md").read_text() + # This artifact is executable guidance: characterize its activation boundary, + # not a dated heading or incidental editorial wording. + assert "If context is absent, disabled, or the read fails" in installed + assert "do not seek delegation splits or invoke child tools" in installed + assert ( + "Consider useful\nread-heavy delegation within a single Todo" not in installed + ) + from loopx.control_plane.agent_context import project_agent_context + + assert ( + project_agent_context( + phase="before_plan", + scope=SCOPE, + orchestration={**POLICY, "spawn_allowed": False}, + ) + is None + ) + enabled = project_agent_context( + phase="before_plan", scope=SCOPE, orchestration=POLICY + ) + assert any( + "parallel delegation" in item + for item in enabled["contributions"][0]["guidance"] + ) diff --git a/tests/control_plane_ts/turn_envelope.test.ts b/tests/control_plane_ts/turn_envelope.test.ts index e8fe6fff03..2dd86d9675 100644 --- a/tests/control_plane_ts/turn_envelope.test.ts +++ b/tests/control_plane_ts/turn_envelope.test.ts @@ -10,6 +10,7 @@ import { turnEnvelopeActionSignatureDocument, } from "../../loopx/control_plane/quota/turn_envelope.ts"; import { EffectRuntimeRequestError } from "../../loopx/control_plane/effect_runtime_errors.ts"; +import { TURN_ENVELOPE_SECTION_TARGETS } from "../../loopx/control_plane/quota/turn_envelope_budget.ts"; function payload(): Record { return { @@ -195,6 +196,21 @@ test("v0 compaction metric preserves Unicode code-point compatibility", () => { ); }); +test("warning accounting converges across decimal-width and ratio boundaries", () => { + assert.equal(Object.values(TURN_ENVELOPE_SECTION_TARGETS).reduce((a, b) => a + b, 0), 8192); + for (let size = 6_000; size < 6_300; size += 1) { + const source = payload(); + source.goal_boundary = { execution_profile: { padding: "界".repeat(size) } }; + const envelope = buildTurnEnvelope({ payload: source, protocol_action_fields: {}, scheduler_execution_args: "" }); + const metric = envelope.compaction as Record; + const bytes = Buffer.byteLength(JSON.stringify(envelope), "utf8"); + assert.equal(metric.envelope_utf8_bytes, bytes); + assert.equal(metric.envelope_json_bytes, [...JSON.stringify(envelope)].length); + assert.equal(Object.values(metric.warning.section_bytes as Record).reduce((a, b) => a + b, 0), bytes); + assert.equal(metric.warning.excess_bytes, bytes - 8192); + } +}); + test("signature key ordering preserves Python Unicode code-point compatibility", () => { const source = { goal_id: "g", diff --git a/tests/test_loop_turn_loop_controller.py b/tests/test_loop_turn_loop_controller.py index ecb08ad93b..5836ec3956 100644 --- a/tests/test_loop_turn_loop_controller.py +++ b/tests/test_loop_turn_loop_controller.py @@ -939,11 +939,11 @@ def test_mismatched_signature_hashes_raise() -> None: decide_loop_disposition(turn_receipt=None, quota_decision=envelope) -def test_over_budget_compaction_raises() -> None: +def test_over_budget_compaction_preserves_disposition() -> None: envelope = _envelope(should_run=True) envelope["compaction"] = {"within_budget": False} - with pytest.raises(ValueError, match="envelope contract"): - decide_loop_disposition(turn_receipt=None, quota_decision=envelope) + result = decide_loop_disposition(turn_receipt=None, quota_decision=envelope) + _assert_markers(result, "run_now") def test_mismatched_signature_raises() -> None: diff --git a/tests/test_loopx_turn_driver.py b/tests/test_loopx_turn_driver.py index 3371dac209..785c6deaa5 100644 --- a/tests/test_loopx_turn_driver.py +++ b/tests/test_loopx_turn_driver.py @@ -1094,7 +1094,7 @@ def test_turn_plan_fails_closed_on_action_signature_drift() -> None: assert payload["route"]["would_invoke_host"] is False -def test_turn_plan_fails_closed_on_oversized_turn_envelope() -> None: +def test_turn_plan_preserves_route_on_budget_warning() -> None: envelope = _envelope() envelope["compaction"] = {"within_budget": False} @@ -1104,8 +1104,9 @@ def test_turn_plan_fails_closed_on_oversized_turn_envelope() -> None: execution_mode="interactive-visible", ) - assert payload["ok"] is False - assert payload["route"]["kind"] == LoopXTurnRoute.CONTRACT_ERROR.value + assert payload["ok"] is True + assert payload["route"]["kind"] == LoopXTurnRoute.READY_FOR_HOST.value + assert payload["turn_envelope"]["compaction"]["within_budget"] is False def test_scheduler_followup_binding_preserves_turn_lineage( diff --git a/tests/test_turn_envelope.py b/tests/test_turn_envelope.py index 56b6db451d..3d703d74da 100644 --- a/tests/test_turn_envelope.py +++ b/tests/test_turn_envelope.py @@ -779,9 +779,7 @@ def test_turn_envelope_stays_actionable_during_scheduler_reset() -> None: assert envelope["action_signature"]["matches"] is True assert envelope["compaction"]["within_budget"] is True assert envelope["compaction"]["envelope_json_bytes"] <= TURN_ENVELOPE_BUDGET_BYTES - assert envelope["compaction"]["envelope_json_bytes"] >= ( - TURN_ENVELOPE_BUDGET_BYTES - 1_024 - ) + # Compression may create additional headroom; there is no minimum size. def test_turn_envelope_keeps_concrete_user_gate() -> None: