From 84d1cd606362a39ba1c911690287ebfd6edce4bf Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:06:56 +0530 Subject: [PATCH 1/2] fix(runtime): preserve UTF-8 RPC context within a measured 8 MiB budget Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../canonical-snapshot-pagination.md | 70 ++++++++- docs/reference/configuration-backup.md | 2 +- .../goal-vision-replan-contract-v0.md | 2 +- docs/reference/protocols/loopx-turn-v0.md | 4 +- docs/reference/research-checkpoint.md | 2 +- .../reviewed-coordination-promotion.md | 7 +- .../reviewed-coordination-promotion.zh-CN.md | 6 +- docs/reference/trajectory-assessment.md | 2 +- .../performance_diagnosis/README.md | 2 +- .../performance_diagnosis/README.zh-CN.md | 2 +- loopx/control_plane/effect_runtime.py | 19 ++- loopx/control_plane/effect_runtime_server.ts | 12 +- .../test_trajectory_assessment_store.py | 6 +- .../test_canonical_snapshot_integration.py | 2 +- .../test_checkpoint_artifact_transport.py | 8 +- .../test_checkpoint_read_context.py | 7 +- .../test_effect_runtime_integration.py | 144 +++++++++++++++++- .../test_goal_state_context_transport.py | 3 +- .../test_handoff_context_projection.py | 2 +- .../control_plane/test_inbox_read_recovery.py | 3 +- .../test_monitor_target_transport.py | 8 +- tests/control_plane/test_quota_selection.py | 9 +- .../test_research_history_capacity.py | 16 +- .../test_work_acceptance_context.py | 5 +- 24 files changed, 289 insertions(+), 54 deletions(-) diff --git a/docs/reference/canonical-snapshot-pagination.md b/docs/reference/canonical-snapshot-pagination.md index c65d692c3e..0998d4fb9b 100644 --- a/docs/reference/canonical-snapshot-pagination.md +++ b/docs/reference/canonical-snapshot-pagination.md @@ -23,8 +23,9 @@ continuation, so a large valid population remains readable without raising a transport limit or retrying a lost oversized response. The standalone wire-only `coordination.local_authority.todo_snapshot_page` endpoint remains compatible. Its pages, and capacity fallback pages, contain up to 4,096 Todo/lease records and -1,792 KiB of UTF-8 JSON. The generic RPC response ceiling remains 2 MiB, leaving 256 KiB for its -envelope. Normal collections can still complete in one page. A record +1,792 KiB of UTF-8 JSON. That page budget still fits the original 2 MiB wire +with 256 KiB for its envelope; it does not grow with the shared 8 MiB RPC ceiling +described below. Normal collections can still complete in one page. A record and its acceptance guard travel together; records are never shortened to meet a budget. Archived dependency/decision records remain in the full collection. @@ -118,3 +119,68 @@ This closes the bounded collection-read part of T3/L5. It does not establish D1 permanent projection freshness, SQLite D2 durability/soak, D3 whole-Goal migration, or default-on qualification. See the [remaining PR sequence](../architecture/rfcs/shared-goal-authority-state-provider-v0.md). + +## Shared RPC byte budget + +Ordinary Python/TypeScript Effect requests and inline responses admit up to +8 MiB, including the newline-delimited envelope. Python emits compact UTF-8 +JSON instead of ASCII-escaping every non-ASCII character. JSON values, escaped +control characters and lone-surrogate compatibility remain intact. The server +counts UTF-8 bytes and searches each new socket chunk for the delimiter, then +parses the complete first frame once. Earlier chunks cannot contain that +delimiter; repeatedly searching the accumulated string needlessly flattens it. + +This is an intentional increase from 2 MiB. A complete digest-bound source +containing Chinese text and emoji occupied 1,000,387 UTF-8 parameter bytes but +2,400,387 ASCII-escaped bytes: the old ordinary call refused before dispatch, +while the explicitly allowed snapshot call wrote the inflated parameters. +The UTF-8 call returns the same complete source, including its final constraint. +It does not shorten Goal text or remove required reads to fit a budget. + +The capacity decision compared 2, 4 and 8 MiB on the same disposable Node 24 +worker with the UTF-8 encoder held fixed. Each trial read complete, hash-bound +1/3/6 MiB sources, then four concurrent 6 MiB sources; a separate Python process +sent small pings. Three uninstrumented trials per variant gave: + +| Ordinary cap and delimiter scan | 6 MiB warm roundtrip (ms) | Worker peak RSS (MiB) | Busy ping observed p95 (ms) | +| --- | ---: | ---: | ---: | +| 2 MiB, accumulated string; snapshot fallback | 68.7–79.5 | 196.8–218.0 | 20.8–26.8 | +| 8 MiB, accumulated string; inline | 79.5–85.8 | 254.7–320.7 | 154.7–203.5 | +| 8 MiB, new chunk only; inline | 57.7–60.0 | 214.6–222.4 | 28.0–66.8 | + +Raising the cap alone was rejected. V8 samples identified the repeated data +callback work; changing only the delimiter scan removed that hotspot, and the +uninstrumented comparison confirmed lower cost. The selected 8 MiB cap lets +the complete 3/6 MiB sources stay inline. The 4 MiB candidate still sent the +6 MiB source through a private file. Larger inline calls retain a concurrency +cost: in a separate four-by-3 MiB workload, busy ping maxima were 72–101 ms +with 8 MiB versus 17–19 ms with 2 MiB and snapshot fallback. These short local +samples are observations, not a stable tail-latency SLO or long-horizon/model +qualification. Roundtrip includes encoding, safe refusal/fallback when needed, +transport and result validation; it excludes fixture preparation and startup. +Peak RSS covers the worker's whole trial, including startup. The four large +callers share one Python process; only the ping caller runs independently. + +The 64 MiB opt-in private-file capacity, method allowlist, same-UID ownership, +permissions, digests, cleanup, deadlines and ambiguous-response recovery remain +unchanged. Over-limit ordinary requests still refuse before dispatch; a possible +commit followed by an oversized or lost response still requires receipt recovery. +Pagination budgets and frozen experiment/promotion criteria remain independent. +These transport changes add no capability, setting, execution or merge authority. +Existing consumers that derive a limit from `MAX_REQUEST_BYTES` follow the same +8 MiB budget: context-capture/native-input file and event readers, Todo summary +and handoff batches, and task-graph/replan-history transport selection. Their +source identity, file/privacy, digest and replay checks remain in force. Host +loading limits, progressive presentation budgets and frozen experiment bounds +are separate; increasing the shared wire does not change them. + +普通 Effect RPC 的请求和内联响应上限从 2 MiB 放宽到 8 MiB,按包含换行封包的 +实际 UTF-8 字节计量。保留完整正文、JSON 值及孤立代理项兼容,不删约束或必要读取。 +同时只扫描新增 socket 块中的换行,避免反复展开累积正文;单独提高上限的内存和 +延迟回退保留为失败对照。8 MiB 可让完整 3/6 MiB 来源走内联路径,但并发大请求仍 +可能增加小请求等待;以上为短时本机观测,不代表稳定尾延迟、真实模型或长程收益 +已验收。64 MiB 私有文件容量、方法和权限边界、摘要、清理、超时及不确定结果恢复 +不变;分页预算与已冻结实验/晋升条件独立,不追认历史失败。 +既有引用共享常量的上下文捕获/原生输入读取、Todo 批次与 task-graph/replan-history +传输选择同步沿用 8 MiB;来源身份、文件隐私、摘要和重放检查保留。宿主加载和渐进 +展示预算各自独立,不随 RPC 上限放宽。 diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index b47ff006bb..6ae30616a6 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -489,7 +489,7 @@ SQLite gets a 64 MiB process-local [hard heap limit](https://www.sqlite.org/prag this is why that verifier is not dispatched in the shared Effect daemon. Views and virtual tables cannot masquerade as authority tables. WAL-mode copies and unsupported/service-owned providers remain unknown and need their existing -owner; PostgreSQL is never contacted. The normal 2 MiB RPC wire is unchanged; +owner; PostgreSQL is never contacted. The shared [8 MiB ordinary RPC wire](canonical-snapshot-pagination.md#shared-rpc-byte-budget) remains bounded; only the two complete-manifest checks opt into the existing 64 MiB private-file transport. These unchanged bounds are approved for local POSIX File/SQLite inert inspection by the [recorded maintainer decision](../architecture/rfcs/complete-state-recovery-v0.md#m1-capacity-default-decision). diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index bab80994bb..4fd53e2ff2 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -1007,7 +1007,7 @@ configured, the current agent vision, and the local source binding. A replan obligation covers the full Todo frontier. Archived dependencies remain inputs. Large local bases use digest-checked private files across the Python/TypeScript runtime boundary, including the response; the CLI still returns the complete -basis. The 2 MiB default RPC guard remains for other effects. File size is +basis. Other effects retain the shared [8 MiB RPC guard](../canonical-snapshot-pagination.md#shared-rpc-byte-budget). File size is bounded and an unverifiable response after a possible commit is ambiguous, so the caller reads the exact receipt before retrying any mutation. Neither transport nor a future paged presentation may silently omit a basis component. diff --git a/docs/reference/protocols/loopx-turn-v0.md b/docs/reference/protocols/loopx-turn-v0.md index c4c0bff3c8..53c0d17ad4 100644 --- a/docs/reference/protocols/loopx-turn-v0.md +++ b/docs/reference/protocols/loopx-turn-v0.md @@ -522,7 +522,7 @@ to preserve integral floats, signed zero and unsafe integers before atomic publication. Replay and immutable-plan comparison distinguish these source numbers; refusals retain the original file. Ordinary objects without metadata retain their encoding and operation identity; historical receipts are not -rewritten. This remains one RPC under the existing 2 MiB request limit and avoids +rewritten. This remains one RPC under the shared [8 MiB request limit](../canonical-snapshot-pagination.md#shared-rpc-byte-budget) and avoids double escaping evidence text. Metadata still costs bytes for number-dense sources. Source fidelity and transport checks do not establish latency or long-horizon benefit. @@ -531,7 +531,7 @@ Python 来源的 journal 快照仍经同一 TypeScript writer 传递 JSON 对象 context delivery 的稀疏数值 IO 元数据与校验器,在原子发布前保留整值浮点、带 符号零和非安全整数。重放与不可变 plan 比较区分这些来源数值;拒绝时保留原 文件。无元数据的普通对象保留已有编码与操作身份,不改写历史回执。仍为一次 -RPC,沿用 2 MiB 请求上限,避免证据正文的双重转义;数值密集来源仍有元数据 +RPC,沿用共享的 8 MiB 请求上限,避免证据正文的双重转义;数值密集来源仍有元数据 字节成本。来源保真与传输检查不代表延迟或长程收益已得到验证。 If executing `run-once` raises unexpectedly, its error response retains the diff --git a/docs/reference/research-checkpoint.md b/docs/reference/research-checkpoint.md index 34ea7e616e..c457d0e6df 100644 --- a/docs/reference/research-checkpoint.md +++ b/docs/reference/research-checkpoint.md @@ -45,7 +45,7 @@ files; arbitrary caller-declared artifacts remain private and are not privacy certified. A new destination is required; capture never overwrites a point. Artifact operations use the existing private local snapshot transport for full -inventories and retained metadata above the ordinary 2 MiB Effect wire limit. +inventories and retained metadata above the ordinary [8 MiB Effect wire limit](canonical-snapshot-pagination.md#shared-rpc-byte-budget). The existing 64 MiB local snapshot bound and original operation deadlines still apply. No witness is truncated, no capture is enabled implicitly, and a lost capture response must be reconciled under its original identity before another diff --git a/docs/reference/reviewed-coordination-promotion.md b/docs/reference/reviewed-coordination-promotion.md index 0c4f7d8872..4cd12bdb95 100644 --- a/docs/reference/reviewed-coordination-promotion.md +++ b/docs/reference/reviewed-coordination-promotion.md @@ -228,13 +228,16 @@ an execution fails; do not infer that a failure left legacy writers usable. ## Bounded capture proof transport -A long Goal can exceed the existing 2 MiB RPC response budget before promotion: +At the original 2 MiB RPC response budget, a long Goal could exceed it before promotion: sequence recovery used to return a full head and full projections for retained transactions. The `outbox_read` proof read model now keeps full lineage validation inside TypeScript, while returning progress, receipts, projection digests and partition markers. Sequence allocation requests no transaction rows; drain uses the compact rows. Existing full diagnostic reads retain their default contract. -No transport limit, stored population or transaction validation is weakened. +That proof-read change did not raise a transport limit, reduce the stored population +or weaken transaction validation. The ordinary wire now uses the independently +measured [shared 8 MiB budget](canonical-snapshot-pagination.md#shared-rpc-byte-budget); +the original refusal remains historical evidence. A pending outbox still blocks promotion; use the existing bounded `authority-shadow drain --goal-id example-goal --budget-seconds 60` operation and inspect its result before retrying preview. diff --git a/docs/reference/reviewed-coordination-promotion.zh-CN.md b/docs/reference/reviewed-coordination-promotion.zh-CN.md index 1d566cf626..f158ce7e71 100644 --- a/docs/reference/reviewed-coordination-promotion.zh-CN.md +++ b/docs/reference/reviewed-coordination-promotion.zh-CN.md @@ -173,10 +173,12 @@ provider revision、receipt 内容和初始 projection 必须全部一致。 ## 大 Goal 的捕获证明 序号恢复以前会返回完整 head 和历史事务的完整 projection,大 Goal 可能因此超过 -现有 2 MiB RPC 响应上限,甚至不能产生晋升所需的 shadow 变更。 +当时的 2 MiB RPC 响应上限,甚至不能产生晋升所需的 shadow 变更。 现在 `outbox_read` 的 proof read model 在 TS 内完整校验事务链,只传回进度、receipt、 projection digest 和 partition marker;分配序号不返回事务行,drain 使用紧凑行。 -原有完整诊断读取保持默认合同,不提高传输上限、不减少持久数据,也不省略链校验。 +原有完整诊断读取保持默认合同;该 proof-read 变更不提高传输上限、不减少持久数据, +也不省略链校验。普通传输现沿用单独测量的[共享 8 MiB 预算](canonical-snapshot-pagination.md#shared-rpc-byte-budget), +原超限仍保留为历史失败。 若 outbox 尚未结清,晋升仍会拒绝。先执行并检查既有的有界 drain: `authority-shadow drain --goal-id example-goal --budget-seconds 60`。 diff --git a/docs/reference/trajectory-assessment.md b/docs/reference/trajectory-assessment.md index 23e546952b..e4b39714e0 100644 --- a/docs/reference/trajectory-assessment.md +++ b/docs/reference/trajectory-assessment.md @@ -789,7 +789,7 @@ counter observations are differenced across Turns; summing the cumulative part of input, and reasoning is part of output, not another additive cost. Complete counter histories use the existing private local snapshot transport -when they exceed the ordinary 2 MiB RPC wire. Only this explicitly allowed +when they exceed the ordinary [8 MiB RPC wire](canonical-snapshot-pagination.md#shared-rpc-byte-budget). Only this explicitly allowed method opts in; reduced snapshots retain the 64 MiB ceiling and digest checks. Oversized captures fail explicitly rather than dropping earlier Turns. diff --git a/loopx/capabilities/performance_diagnosis/README.md b/loopx/capabilities/performance_diagnosis/README.md index b869800486..4d54baf7e6 100644 --- a/loopx/capabilities/performance_diagnosis/README.md +++ b/loopx/capabilities/performance_diagnosis/README.md @@ -82,7 +82,7 @@ Input files are limited to 16 MiB; shorten captures beyond that limit. Repeated profile names and hotspot labels share a 1 Mi-character display-text budget; oversized summaries are rejected explicitly, never silently truncated. Larger recordings reuse Effect's private local snapshot transport without raising the -ordinary 2 MiB message limit or returning the raw profile in the summary. +ordinary [8 MiB message limit](../../../docs/reference/canonical-snapshot-pagination.md#shared-rpc-byte-budget) or returning the raw profile in the summary. Unknown source locations exported as `file: null` or `line: null` are omitted; incorrect non-null types remain errors. Repeated V8 leaves are aggregated before ancestor traversal, preserving sample counts and self/inclusive weights. One diff --git a/loopx/capabilities/performance_diagnosis/README.zh-CN.md b/loopx/capabilities/performance_diagnosis/README.zh-CN.md index 28771b0ec5..47209e7bff 100644 --- a/loopx/capabilities/performance_diagnosis/README.zh-CN.md +++ b/loopx/capabilities/performance_diagnosis/README.zh-CN.md @@ -50,7 +50,7 @@ Memray/V8 heap 用工具自己的 allocation reporter。其余工具是后续诊 进程执行时长。 输入限 16 MiB,超出时缩短采集;重复展示的 profile 名称与热点标签共用 1 Mi 字符 上限,超出时明确拒绝,不静默截断。较大的录制复用 Effect 的私有本地快照传输, -不会提高普通命令的 2 MiB 传输上限,也不会把原始 profile 返回到展示结果。 +不会提高普通命令的 [8 MiB 传输上限](../../../docs/reference/canonical-snapshot-pagination.md#shared-rpc-byte-budget),也不会把原始 profile 返回到展示结果。 导出器用 `file: null` 或 `line: null` 表示未知位置时,读取器按缺省处理;非空 错误类型仍拒绝。V8 重复 leaf 先聚合再遍历祖先,保留原样本数及 self/inclusive 权重。整份请求的 frame 分配、sample/event 访问与栈展开共用 2,000,000 工作单位 diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index 76aa51b9d2..6d4ab200b8 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -40,8 +40,8 @@ EFFECT_RUNTIME_STARTUP_ERROR_SCHEMA_VERSION = ( "loopx_effect_runtime_startup_error_v0" ) -MAX_RESPONSE_BYTES = 2 * 1024 * 1024 -MAX_REQUEST_BYTES = 2 * 1024 * 1024 +MAX_RESPONSE_BYTES = 8 * 1024 * 1024 +MAX_REQUEST_BYTES = 8 * 1024 * 1024 MAX_LOCAL_SNAPSHOT_BYTES = 64 * 1024 * 1024 LOCAL_SNAPSHOT_METHODS = frozenset({ "todo.read", @@ -945,6 +945,13 @@ def _stop_effect_runtime( } +def _encode_rpc_json(value: Mapping[str, Any]) -> bytes: + # Count and transmit UTF-8 bytes; lone surrogates retain valid JSON escapes. + return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode( + "utf-8", errors="backslashreplace", + ) + + def _request_with_info( info: Mapping[str, Any], *, @@ -974,9 +981,9 @@ def _request_with_info( descriptor = os.open(response_sink, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) os.close(descriptor) request["response_sink"] = str(response_sink) - encoded = (json.dumps(request, separators=(",", ":")) + "\n").encode() + encoded = _encode_rpc_json(request) + b"\n" if len(encoded) > MAX_REQUEST_BYTES: - params_bytes = json.dumps(dict(params), separators=(",", ":")).encode() + params_bytes = _encode_rpc_json(dict(params)) if len(params_bytes) > MAX_LOCAL_SNAPSHOT_BYTES: raise EffectRuntimeRejected( "TypeScript Effect runtime local snapshot is oversized", @@ -992,9 +999,9 @@ def _request_with_info( "path": str(params_path), "byte_count": len(params_bytes), "sha256": hashlib.sha256(params_bytes).hexdigest(), } - encoded = (json.dumps(request, separators=(",", ":")) + "\n").encode() + encoded = _encode_rpc_json(request) + b"\n" else: - encoded = (json.dumps(request, separators=(",", ":")) + "\n").encode() + encoded = _encode_rpc_json(request) + b"\n" if len(encoded) > MAX_REQUEST_BYTES: raise EffectRuntimeRejected( "TypeScript Effect runtime request is oversized", diff --git a/loopx/control_plane/effect_runtime_server.ts b/loopx/control_plane/effect_runtime_server.ts index e8a0de86e8..cfb2465ff2 100644 --- a/loopx/control_plane/effect_runtime_server.ts +++ b/loopx/control_plane/effect_runtime_server.ts @@ -24,9 +24,9 @@ const REQUEST_SCHEMA = "loopx_effect_runtime_request_v0"; const RESPONSE_SCHEMA = "loopx_effect_runtime_response_v1"; const INFO_SCHEMA = "loopx_effect_runtime_info_v0"; const STARTUP_ERROR_SCHEMA = "loopx_effect_runtime_startup_error_v0"; -const MAX_REQUEST_BYTES = 2 * 1024 * 1024; -const MAX_INLINE_RESPONSE_BYTES = 2 * 1024 * 1024; -// Explicit opt-in: ordinary effects retain the 2 MiB request/response wire. +const MAX_REQUEST_BYTES = 8 * 1024 * 1024; +const MAX_INLINE_RESPONSE_BYTES = 8 * 1024 * 1024; +// Explicit opt-in: ordinary effects retain a bounded request/response wire. const LOCAL_SNAPSHOT_METHODS = new Set([ "todo.read", "todo.quota_planning.project", @@ -186,14 +186,16 @@ const server = createServer((socket) => { request_id: "unknown", ok: false, error: effectRuntimeErrorPayload(new EffectRuntimeRequestError( - "Effect runtime request exceeds the 2 MiB limit", + "Effect runtime request exceeds the 8 MiB limit", "request_too_large", )), })}\n`); return; } raw += chunk; - if (!raw.includes("\n")) return; + // Earlier chunks had no delimiter. Scanning the accumulated string on each + // chunk repeatedly flattens it; only the newly received chunk can finish it. + if (!chunk.includes("\n")) return; socket.pause(); pendingRequests += 1; clearTimeout(idleTimer); diff --git a/tests/capabilities/test_trajectory_assessment_store.py b/tests/capabilities/test_trajectory_assessment_store.py index 3fa1928a37..24e4b65ea4 100644 --- a/tests/capabilities/test_trajectory_assessment_store.py +++ b/tests/capabilities/test_trajectory_assessment_store.py @@ -1156,7 +1156,7 @@ def test_large_usage_real_cli_register_readback_and_replay(pack, monkeypatch): ) assert ( len(json.dumps(params, separators=(",", ":")).encode()) - > effect_runtime.MAX_REQUEST_BYTES + > 2 * 1024 * 1024 # Keep the original complete 4,500-Turn counter history. ) registry = sources.parent / "registry.json" registry.write_text('{"projects":{}}') @@ -1220,7 +1220,9 @@ def cli(*args): assert "solver_usage" not in inspect_pack(**scope, pack_id=old["pack_id"]) assert view["job"] is None and view["result"] is None - # The method opts into a bounded channel, not a larger ordinary RPC wire. + # Keep the original 2 MiB refusal and reduced private-channel negative. + # The complete counter history above qualifies the current default wire. + monkeypatch.setattr(effect_runtime, "MAX_REQUEST_BYTES", 2 * 1024 * 1024) with pytest.raises( effect_runtime.EffectRuntimeRejected, match="request is oversized" ): diff --git a/tests/control_plane/test_canonical_snapshot_integration.py b/tests/control_plane/test_canonical_snapshot_integration.py index 786491906a..5e9fe7d33c 100644 --- a/tests/control_plane/test_canonical_snapshot_integration.py +++ b/tests/control_plane/test_canonical_snapshot_integration.py @@ -63,7 +63,7 @@ def wide_goal(tmp_path, monkeypatch, request): "status": "open", "done": False, "text": f"Retained work {index}", - "note": "完整🙂" * 1600, + "note": "完整🙂" * 6000, "archive_state": "active", "source_section": "Agent Todo", "index": index + 1, diff --git a/tests/control_plane/test_checkpoint_artifact_transport.py b/tests/control_plane/test_checkpoint_artifact_transport.py index 6bb1f4a3e3..0de71f48e3 100644 --- a/tests/control_plane/test_checkpoint_artifact_transport.py +++ b/tests/control_plane/test_checkpoint_artifact_transport.py @@ -11,7 +11,6 @@ MAX_RESPONSE_BYTES, EffectRuntimeRejected, EffectRuntimeRemoteError, - EffectRuntimeResponseAmbiguous, effect_runtime_result, ) @@ -38,11 +37,12 @@ def test_large_member_transport_with_explicit_rpc_budget_preserves_inventory(tmp witness = {"research": expected} # The expected inventory is independent of the production walker. A count # alone would miss dropped, reordered or substituted scientific members. - assert len(json.dumps(witness, separators=(",", ":")).encode()) > MAX_RESPONSE_BYTES + # Preserve this original inventory: it now also fits the ordinary wire. + assert 2 * 1024 * 1024 < len(json.dumps(witness, separators=(",", ":")).encode()) <= MAX_RESPONSE_BYTES components = [{"id": "research", "source_path": str(source)}] request = {"action": "probe", "components": components} - with pytest.raises(EffectRuntimeResponseAmbiguous): - effect_runtime_result("runtime.checkpoint_artifacts", request, timeout=LARGE_ARTIFACT_RPC_SECONDS) + inline = effect_runtime_result("runtime.checkpoint_artifacts", request, timeout=LARGE_ARTIFACT_RPC_SECONDS) + assert inline["source_witness"] == witness observed = effect_runtime_result("runtime.checkpoint_artifacts", request, timeout=LARGE_ARTIFACT_RPC_SECONDS, large_local_snapshot=True) assert observed["source_witness"] == witness diff --git a/tests/control_plane/test_checkpoint_read_context.py b/tests/control_plane/test_checkpoint_read_context.py index dd101cd4f5..b9619f978e 100644 --- a/tests/control_plane/test_checkpoint_read_context.py +++ b/tests/control_plane/test_checkpoint_read_context.py @@ -103,7 +103,6 @@ def test_large_goal_context_keeps_complete_basis_and_checkpoint_only_recovery( ): from canonical_authority_fixture import advance_canonical_fixture_authority, isolate_sqlite_runtime from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection - from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES if provider == "sqlite": isolate_sqlite_runtime(tmp_path, monkeypatch) @@ -118,10 +117,10 @@ def test_large_goal_context_keeps_complete_basis_and_checkpoint_only_recovery( projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=[todo]) advance_canonical_fixture_authority(runtime, GOAL_ID, projection, operation_id="fixture-checkpoint-context") # The original failure came from complete source prose, not from the - # small prior Turn receipt. Keep the source intact and cross both 2 MiB - # request and response boundaries through the real local CLI/runtime. + # small prior Turn receipt. Retain the original over-2-MiB workload even + # when a larger ordinary wire lets it travel inline. large_prose = "This is retained Goal context.\n" * 115_000 - assert len(large_prose.encode()) > MAX_REQUEST_BYTES + assert len(large_prose.encode()) > 2 * 1024 * 1024 state.write_text(state.read_text(encoding="utf-8") + "\n## Context\n\n" + large_prose, encoding="utf-8") rc, context = _run_cli(registry, runtime, "checkpoint-context", *binding, cwd=project) diff --git a/tests/control_plane/test_effect_runtime_integration.py b/tests/control_plane/test_effect_runtime_integration.py index 9a259cf3d2..5b8f42de61 100644 --- a/tests/control_plane/test_effect_runtime_integration.py +++ b/tests/control_plane/test_effect_runtime_integration.py @@ -1922,6 +1922,122 @@ def test_valid_idle_timeout_configuration_serves_requests( pass +@pytest.mark.parametrize("text", [ + "quotes: \"\\\n\r\t\x00; Unicode: 中文🙂\u2028\u2029", + "lone: \ud800; low: \udfff; paired: \ud83d\ude42; backslash: \\ud800", +]) +def test_rpc_json_preserves_legacy_values_with_valid_utf8(text: str) -> None: + value = {"text": text, "nested": [None, True, 0, -1, 1.25]} + encoded = effect_runtime._encode_rpc_json(value) + assert json.loads(encoded.decode("utf-8")) == json.loads(json.dumps(value)) + assert b"\n" not in encoded, "newlines inside values must not end the frame" + if "中文" in text: + assert "中文🙂".encode() in encoded + else: + assert b"\\ud800" in encoded and b"\\udfff" in encoded + + ascii_value = {"text": "quote: \"; slash: \\; line: \n", "count": 17} + assert effect_runtime._encode_rpc_json(ascii_value) == json.dumps( + ascii_value, separators=(",", ":"), + ).encode("ascii") + + +@pytest.mark.parametrize("repeats,snapshot", [(380_000, False), (900_000, True)]) +def test_complete_unicode_context_uses_actual_wire_bytes_and_snapshot_fallback( + tmp_path: Path, monkeypatch, repeats: int, snapshot: bool, +) -> None: + monkeypatch.setattr(effect_runtime, "_runtime_dir", lambda: tmp_path / "runtime") + # The smaller source fits 8 MiB in UTF-8 but exceeds it when ASCII-escaped. + text = "保留🙂" * repeats + "\nFinal constraint: retain the original Goal." + params = { + "required_reads": [{"source": "goal_state", "command": "read synthetic goal"}], + "source_results": [{"command": "read synthetic goal", "content": { + "text": text, "full_source": True, "state_file": "synthetic-goal.md", + "revision": "sha256:" + hashlib.sha256(text.encode()).hexdigest(), + }}], + "require_full_goal_source": True, + } + assert len(json.dumps(params).encode()) > 8 * 1024 * 1024 + original_send = socket.socket.sendall + captured: list[dict[str, object]] = [] + temporary_paths: list[Path] = [] + + def observe_send(connection, data, flags=0): + request = json.loads(data) + if request["method"] == "work_item.context.project": + captured.append(request) + assert len(data) <= 8 * 1024 * 1024 + if "params_ref" in request: + ref = request["params_ref"] + path = Path(ref["path"]) + body = path.read_bytes() + assert ref["byte_count"] == len(body) + assert ref["sha256"] == hashlib.sha256(body).hexdigest() + assert "保留🙂".encode() in body and json.loads(body) == params + temporary_paths.extend([path, Path(request["response_sink"])]) + else: + assert "保留🙂".encode() in data and request["params"] == params + return original_send(connection, data, flags) + + monkeypatch.setattr(socket.socket, "sendall", observe_send) + try: + result = effect_runtime.effect_runtime_result( + "work_item.context.project", params, large_local_snapshot=snapshot, + ) + assert result["required_reads"] == [] + assert result["work_context"]["sources"][0]["content"]["text"] == text + assert len(captured) == 1 + assert ("params_ref" in captured[0]) is snapshot + assert all(not path.exists() for path in temporary_paths) + assert effect_runtime.effect_runtime_result("runtime.ping", {})["pid"] > 0 + finally: + effect_runtime.effect_runtime_result("runtime.shutdown", {}, retry_safe=False) + + +def test_fragmented_utf8_request_dispatches_only_the_first_complete_frame( + tmp_path: Path, monkeypatch, +) -> None: + monkeypatch.setattr(effect_runtime, "_runtime_dir", lambda: tmp_path / "runtime") + ping = effect_runtime.effect_runtime_result("runtime.ping", {}) + fingerprint = effect_runtime._runtime_fingerprint() + info = effect_runtime._read_info( + effect_runtime._runtime_info_path(fingerprint), fingerprint=fingerprint, + ) + assert info is not None + first = json.dumps({ + "schema_version": effect_runtime.EFFECT_RUNTIME_REQUEST_SCHEMA_VERSION, + "token": info["token"], "request_id": "fragmented", "method": "runtime.ping", + "params": {"text": "界🙂\nmore"}, + }, ensure_ascii=False).encode() + second = json.dumps({ + "schema_version": effect_runtime.EFFECT_RUNTIME_REQUEST_SCHEMA_VERSION, + "token": info["token"], "request_id": "ignored", "method": "runtime.shutdown", + "params": {}, + }).encode() + b"\n" + split = first.index("界".encode()) + try: + with socket.create_connection((info["host"], info["port"]), timeout=2) as connection: + connection.settimeout(.02) + # Split inside a UTF-8 code point and withhold the physical newline. + for piece in [first[:split + 1], first[split + 1:split + 2], first[split + 2:]]: + connection.sendall(piece) + with pytest.raises(socket.timeout): + connection.recv(1) + connection.sendall(b"\n" + second) + connection.settimeout(2) + response = b"" + while b"\n" not in response: + chunk = connection.recv(64 * 1024) + assert chunk + response += chunk + answer = json.loads(response) + assert answer["ok"] is True and answer["request_id"] == "fragmented" + assert answer["result"]["pid"] == ping["pid"] + assert effect_runtime.effect_runtime_result("runtime.ping", {})["pid"] == ping["pid"] + finally: + effect_runtime.effect_runtime_result("runtime.shutdown", {}, retry_safe=False) + + def test_oversized_request_is_rejected_before_runtime_dispatch( tmp_path: Path, monkeypatch, @@ -2051,7 +2167,7 @@ def test_oversized_raw_socket_request_returns_typed_error( "error": { "kind": "request_rejected", "code": "request_too_large", - "message": "Effect runtime request exceeds the 2 MiB limit", + "message": "Effect runtime request exceeds the 8 MiB limit", }, } assert ( @@ -2061,6 +2177,32 @@ def test_oversized_raw_socket_request_returns_typed_error( effect_runtime.effect_runtime_result("runtime.shutdown", {}, retry_safe=False) +def test_raw_request_accepts_the_inclusive_eight_mib_wire_boundary( + tmp_path: Path, monkeypatch, +) -> None: + monkeypatch.setattr(effect_runtime, "_runtime_dir", lambda: tmp_path / "runtime") + ping = effect_runtime.effect_runtime_result("runtime.ping", {}) + fingerprint = effect_runtime._runtime_fingerprint() + info = effect_runtime._read_info( + effect_runtime._runtime_info_path(fingerprint), fingerprint=fingerprint, + ) + assert info is not None + envelope = json.dumps({ + "schema_version": effect_runtime.EFFECT_RUNTIME_REQUEST_SCHEMA_VERSION, + "token": info["token"], "request_id": "at-limit", "method": "runtime.ping", + "params": {}, + }).encode() + # JSON trailing whitespace is legal; count the entire frame, including LF. + payload = envelope + b" " * (8 * 1024 * 1024 - len(envelope) - 1) + b"\n" + try: + answer = _raw_runtime_response(info, payload) + assert answer["ok"] is True and answer["request_id"] == "at-limit" + assert answer["result"]["pid"] == ping["pid"] + assert effect_runtime.effect_runtime_result("runtime.ping", {})["pid"] == ping["pid"] + finally: + effect_runtime.effect_runtime_result("runtime.shutdown", {}, retry_safe=False) + + def test_non_object_json_request_is_rejected_at_the_socket_boundary( tmp_path: Path, monkeypatch, diff --git a/tests/control_plane/test_goal_state_context_transport.py b/tests/control_plane/test_goal_state_context_transport.py index 7a734abef4..62a6cc05c3 100644 --- a/tests/control_plane/test_goal_state_context_transport.py +++ b/tests/control_plane/test_goal_state_context_transport.py @@ -13,7 +13,6 @@ from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime from loopx.cli import main from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection -from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES from loopx.control_plane.testing.canary_harness import write_fixture_registry from loopx.control_plane.work_items import context_readback @@ -109,7 +108,7 @@ def guard(): assert state.read_bytes() == initial large = initial + b"\n## Evidence\n" + b"Prior evidence remains available. " * 100_000 large += "\n## Stop Conditions\n未经授权不得发布。\n".encode() - assert len(large) > MAX_REQUEST_BYTES + assert len(large) > 2 * 1024 * 1024 # Preserve the original source regression. state.write_bytes(large) large_channel = guard() large_request = requests[-1] diff --git a/tests/control_plane/test_handoff_context_projection.py b/tests/control_plane/test_handoff_context_projection.py index 55223657ee..e9b9003bd1 100644 --- a/tests/control_plane/test_handoff_context_projection.py +++ b/tests/control_plane/test_handoff_context_projection.py @@ -157,7 +157,7 @@ def track(method, request, **kwargs): monkeypatch.setattr(effect_runtime, "effect_runtime_result", track) rows = [{"todo_id": f"todo_large_{i}", "text": "Work", "status": "open", - "task_class": "advancement_task", "note": "a" * 120000} for i in range(20)] + "task_class": "advancement_task", "note": "a" * 480000} for i in range(20)] result = compact_todo_summary_items(rows) assert len(result) == 20 assert [row["todo_id"] for row in result] == [row["todo_id"] for row in rows] diff --git a/tests/control_plane/test_inbox_read_recovery.py b/tests/control_plane/test_inbox_read_recovery.py index 80bc57e6d7..c94ab44b7d 100644 --- a/tests/control_plane/test_inbox_read_recovery.py +++ b/tests/control_plane/test_inbox_read_recovery.py @@ -102,7 +102,8 @@ def test_large_legal_inbox_page_keeps_complete_source_and_records_supplied_reads response = read_inbox(tmp_path, registry, "research", "worker", workspace=tmp_path) assert len(response["items"]) == 20 - assert len(json.dumps(response, ensure_ascii=False).encode()) > MAX_RESPONSE_BYTES + # Keep the original legal page; it now fits the larger ordinary wire. + assert 2 * 1024 * 1024 < len(json.dumps(response, ensure_ascii=False).encode()) <= MAX_RESPONSE_BYTES assert all(item["message"] == message and item["brief"] == normalized["brief"] for item in response["items"]) assert all(item["recorded_decision"] is None for item in response["items"]) assert {p: p.read_bytes() for p in before} == before diff --git a/tests/control_plane/test_monitor_target_transport.py b/tests/control_plane/test_monitor_target_transport.py index 1111aae45a..70974708d4 100644 --- a/tests/control_plane/test_monitor_target_transport.py +++ b/tests/control_plane/test_monitor_target_transport.py @@ -29,13 +29,13 @@ def _monitor(**fields): @pytest.mark.parametrize("provider", ["file", "sqlite"]) -@pytest.mark.parametrize("target_repetitions", [1, 40_000, 120_000]) +@pytest.mark.parametrize("target_repetitions", [1, 40_000, 120_000, 360_000]) def test_real_large_authority_returns_complete_target(tmp_path, monkeypatch, provider, target_repetitions): isolate_sqlite_runtime(tmp_path, monkeypatch) registry, runtime, state = _write_fixture(tmp_path, canonical=False) goal_source = state.read_text(encoding="utf-8") note = "Measured evidence 演示 " * target_repetitions - if target_repetitions == 120_000: + if target_repetitions == 360_000: assert len(note.encode()) > MAX_REQUEST_BYTES target = _monitor(note=note, evidence="source:retained-result", required_capabilities=["network"], resume_when="todo_done:todo_input", @@ -48,13 +48,13 @@ def test_real_large_authority_returns_complete_target(tmp_path, monkeypatch, pro for i in range(320) ] + [target] items[0]["todo_id"] = "todo_input" - assert len(json.dumps(items, ensure_ascii=False).encode()) > MAX_REQUEST_BYTES + assert len(json.dumps(items, ensure_ascii=False).encode()) > 2 * 1024 * 1024 projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=items, handoff_mode="soft_claim") initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider) state.unlink() # No legacy Markdown fallback can satisfy the lookup. - # A record beyond the inline 2 MiB request ceiling is still complete: the + # The largest record exceeds the current ordinary wire and is complete: the # canonical adapter reads through the opt-in private-file entrypoint # (62 MiB), which never truncates a record or substitutes display Markdown. # The wire-only page endpoint keeps its own per-page refusal. diff --git a/tests/control_plane/test_quota_selection.py b/tests/control_plane/test_quota_selection.py index cf80ebaaca..5979aa2854 100644 --- a/tests/control_plane/test_quota_selection.py +++ b/tests/control_plane/test_quota_selection.py @@ -144,7 +144,7 @@ def test_large_history_completes_public_quota_through_vision_wait(tmp_path, monk @pytest.mark.parametrize("gate_scope", [False, True]) -def test_large_quota_history_uses_snapshot_without_losing_scope( +def test_large_quota_history_retains_complete_scope( monkeypatch, gate_scope, ): from loopx.control_plane import effect_runtime @@ -192,10 +192,9 @@ def observe(info, **kwargs): filter_user_gate_blocks_agent=gate_scope, ) - # Unique historical payloads cannot be reduced below the inline wire limit - # without dropping facts. Use the existing digest-verified local snapshot - # transport rather than raising or truncating. - assert observed_sizes and max(observed_sizes) > effect_runtime.MAX_REQUEST_BYTES + # Retain the original over-2-MiB history and its scoped tail. A larger wire + # may carry it inline; either route must preserve the same facts. + assert observed_sizes and max(observed_sizes) > 2 * 1024 * 1024 assert [row["todo_id"] for row in result["gate_open_items"]] == [ "todo_tail_gate" ] diff --git a/tests/control_plane/test_research_history_capacity.py b/tests/control_plane/test_research_history_capacity.py index fb854e7007..a3fe746faf 100644 --- a/tests/control_plane/test_research_history_capacity.py +++ b/tests/control_plane/test_research_history_capacity.py @@ -222,9 +222,23 @@ def open_rows(frontier): restart_effect_runtime() -def test_oversized_active_lineage_is_a_capacity_error_not_an_invalid_graph( +def test_original_client_capacity_refusal_is_not_an_invalid_graph( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + from loopx.capabilities.explore import research_evidence + from loopx.control_plane import effect_runtime + + original = research_evidence.effect_runtime_result + + def original_request_budget(method, params): + # Freeze this regression's original 2 MiB client refusal. Enlarging the + # default wire must not relabel capacity errors as invalid graph data. + # Canonical reads outside this research call retain their real budget. + with monkeypatch.context() as budget: + budget.setattr(effect_runtime, "MAX_REQUEST_BYTES", 2 * 1024 * 1024) + return original(method, params) + + monkeypatch.setattr(research_evidence, "effect_runtime_result", original_request_budget) isolate_sqlite_runtime(tmp_path, monkeypatch) try: state, runtime, registry = goal_fixture(tmp_path, HARNESS) diff --git a/tests/control_plane/test_work_acceptance_context.py b/tests/control_plane/test_work_acceptance_context.py index 417040fc03..6103c32c53 100644 --- a/tests/control_plane/test_work_acceptance_context.py +++ b/tests/control_plane/test_work_acceptance_context.py @@ -190,9 +190,8 @@ def test_acceptance_observation_does_not_transport_the_large_selected_body(tmp_p "text": "Retain this complete requirement. " * 100_000 + "Do not omit this tail.", "status": "open", "done": False, "task_class": "advancement_task", "action_kind": "implement", "index": 1, "source_section": "Agent Todo", "archive_state": "active", "claimed_by": "agent-a"} - from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES - - assert len(todo["text"].encode()) > MAX_REQUEST_BYTES + # The observation must omit unused bodies even if the wire can hold them. + assert len(todo["text"].encode()) > 2 * 1024 * 1024 initialize_canonical_authority(runtime, "large-acceptance-context", build_todo_runtime_shadow_projection(goal_id="large-acceptance-context", todos=[todo], handoff_mode="soft_claim"), state_path=state, provider=provider) From 88a1b6196460dacb7e2c52a043b5726328612ec1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:18:36 +0530 Subject: [PATCH 2/2] test(context): qualify large Unicode capture in native reader Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_saved_context_project.py | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/tests/control_plane/test_saved_context_project.py b/tests/control_plane/test_saved_context_project.py index d6ecc1beab..d6cec670a5 100644 --- a/tests/control_plane/test_saved_context_project.py +++ b/tests/control_plane/test_saved_context_project.py @@ -498,7 +498,7 @@ def consume(wire): @pytest.mark.skipif(not os.environ.get("LOOPX_NATIVE_CHECKPOINT_BIN"), reason="Explicit native Codex; loopback inference only") -@pytest.mark.parametrize("host", ["app_server", "benchmark", "benchmark_progressive", "app_machine", "app_code_mode", "app_progressive"]) +@pytest.mark.parametrize("host", ["app_server", "benchmark", "benchmark_progressive", "app_machine", "app_code_mode", "app_progressive", "app_progressive_large"]) def test_native_saved_preflight_uses_lifecycle_without_guard_replay(tmp_path, monkeypatch, host): from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime, read_fixture_todos from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection @@ -515,7 +515,10 @@ def test_native_saved_preflight_uses_lifecycle_without_guard_replay(tmp_path, mo isolate_sqlite_runtime(tmp_path, monkeypatch) benchmark = host in ("benchmark", "benchmark_progressive") - progressive = host in ("app_progressive", "benchmark_progressive") + progressive = host in ("app_progressive", "benchmark_progressive", "app_progressive_large") + large_capture = host == "app_progressive_large" + unicode_requirement = "保留原始目标、范围与停止条件🙂\n" * 120 + supporting_material = "资料🙂" * 390_000 if large_capture else "" binary, env, workspace, home = _native_fixture(tmp_path) env.update(NODE_OPTIONS="--experimental-sqlite", LOOPX_USAGE_PING="0") env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) @@ -533,8 +536,16 @@ def test_native_saved_preflight_uses_lifecycle_without_guard_replay(tmp_path, mo code, decision = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "saved-native-turn", "--codex-app", cwd=project) assert code == 0, decision + if large_capture: + # Unknown supporting fields are part of the saved capture's + # forward-compatible lossless contract. Only the requested field + # should enter model input; the full material must remain recoverable. + decision["retained_material"] = { + "requirement": unicode_requirement, "supporting": supporting_material} source = workspace / (name + "-source.json") source.write_text(json.dumps(decision, ensure_ascii=False)) + if large_capture: + assert 2 * 1024 * 1024 < source.stat().st_size < 8 * 1024 * 1024 sources.append((source, source.read_bytes())) receipts = _heartbeat_receipt_count(runtime, "saved-native-turn") context, cache = workspace / "context", workspace / "cache" @@ -557,13 +568,21 @@ def test_native_saved_preflight_uses_lifecycle_without_guard_replay(tmp_path, mo phase, queue, observed, errors = "cold", [], {}, [] read_offsets = {} actual_turn = None + rejected_read = False def response(request, number, message): - nonlocal phase, queue, actual_turn, receipts + nonlocal phase, queue, actual_turn, receipts, rejected_read try: if continuation := native_process_continuation(request, f"projection_{number}", code_mode=host == "app_code_mode"): return continuation + if rejected_read: + output = [item["output"] for item in request["input"] + if item.get("type") in ("function_call_output", "custom_tool_call_output")][-1] + rejected = json.loads(output.split("\nOutput:\n", 1)[-1]) + assert rejected["ok"] is False and "content" not in rejected + assert "digest" in rejected["reason"] + rejected_read = False if queue: command = queue.pop(0) elif phase in ("cold", "warm"): @@ -594,7 +613,10 @@ def response(request, number, message): output = [part["text"] for part in output if part.get("type") == "input_text"][-1] navigation = json.loads(output.split("\nOutput:\n", 1)[-1]) descriptor = machine_reference_oracle(navigation) - assert descriptor["ok"] and descriptor["mode"] == ("full" if name == "cold" else "delta") + # A selectively read large capture cannot become a retained + # full baseline; its next projection must remain full. + assert descriptor["ok"] and descriptor["mode"] == ( + "full" if name == "cold" or large_capture else "delta") if benchmark: assert len(json.dumps(navigation, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 32000 data = Path(descriptor["path"]).read_bytes() @@ -613,7 +635,21 @@ def response(request, number, message): # reader in native model input; never consume either file in full. assert "content" not in descriptor queue = [navigation["recovery"]["command"] + " --list", navigation["recovery"]["command"] - + " --select /goal_id --select /heartbeat_receipt"] + + " --select /goal_id --select /heartbeat_receipt" + + (" --select /retained_material/requirement" if large_capture else "")] + if large_capture: + # Reject a different digest through the real native CLI, + # then continue with the unchanged original reference. + argv = shlex.split(navigation["recovery"]["command"]) + index = argv.index("--reference-file") + 1 + reference = json.loads(Path(argv[index]).read_text()) + reference["sha256"] = "0" * 64 + damaged = workspace / (name + "-damaged.reader.json") + damaged.write_text(json.dumps(reference)) + damaged.chmod(0o600) + argv[index] = str(damaged) + queue.insert(0, shlex.join(argv)) + rejected_read = True phase = name + "_read" if not queue: return response(request, number, message) @@ -628,6 +664,11 @@ def response(request, number, message): assert values["/goal_id"] == GOAL_ID assert values["/heartbeat_receipt"]["turn_instance_id"] == ( actual_turn if benchmark else "saved-native-turn") + if large_capture: + assert page["offset"] == 0 and page["eof"] + assert len(page["content"].encode()) == page["total_bytes"] + assert values["/retained_material/requirement"] == unicode_requirement + assert "资料🙂" not in _input_text(request) native_context = home / "loopx-context" if benchmark else context assert "candidate" not in json.loads((native_context / "session.json").read_bytes()) else: @@ -742,6 +783,9 @@ def response(request, number, message): assert current["target"] == descriptor["target"] restored = restore_context_delivery(wire, baseline=baseline, current=current) assert restored["context"] == projected + if large_capture: + assert restored["context"]["retained_material"] == { + "requirement": unicode_requirement, "supporting": supporting_material} if wire["mode"] == "delta": assert restored["mode"] == "delta" # A fallback would hide a broken baseline chain. # Match the native journal's exact prior full capture and depth. The