From 00b7358662d161f5558022db58c4223f8151f1b1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 30 Oct 2026 02:21:08 +0800 Subject: [PATCH 1/6] fix(context): retain acceptance references when owners move Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 8 +++ loopx/control_plane/quota/cli_projection.py | 39 ++++++++---- .../test_quota_acceptance_references.py | 60 +++++++++++++++++++ 3 files changed, 97 insertions(+), 10 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index ffc05aaaea..d60cc87e8f 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -34,6 +34,11 @@ relations and authority revision without repeating the body. If no other canonical fields need a source record, `work_context.selected_todo_authority` carries the source and revision. When the view is bounded, the full canonical detail remains in `work_context.sources`. +The ordinary CLI shares exact acceptance copies within one response. Existing +references follow their complete inline owner when that projection relocates it, +including an owner visited later. Different revisions, types, failures and +sources stay distinct. This adds no source read or authority, admission or +settlement change; saved captures retain their original contents. Registered Goal state uses the existing writer's strict generated-region ownership. When every recognized Todo region is explicitly marked, the source adapter delivers **all other text verbatim** in `work_context.sources`: Goal @@ -579,6 +584,9 @@ all-Goal adoption, actual solver costs or outcome benefit. 普通 Heartbeat/quota 默认在 `agent_channel.work_context` 返回完整选定 Todo、 启用的验收与当前 Agent 相关的 User Todo,附带来源和修订,不需要开启 TurnEnvelope。 +普通 CLI 在同一响应内共享完全相同的验收副本。已有引用跟随本次投影迁移的完整 +内联 owner,包括后访问的 owner;不同修订、类型、失败和来源分别保留。不增加来源 +读取,不改变权限、准入或结算;已存 capture 保留原始内容。 Goal Markdown 复用原 writer 对生成区域的严格归属规则:所有已识别 Todo 区域均有 显式标记时,`work_context.sources` 原样携带区域外的全部文字,保留目标、frontmatter、 任意章节、注释、历史和末尾停止条件。`excluded_todo_regions` 保留每段的角色、行区间 diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 335e9abd4e..72c8a6f501 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -2,7 +2,7 @@ import json from copy import deepcopy -from typing import Any +from typing import Any, cast from .decision_summary import compact_quota_decision from .should_run_packet import reference_duplicate_fallback_guidance @@ -504,7 +504,21 @@ def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any] ("agent_todo_summary", "goal_acceptance_contract"), ]) carriers: dict[str, str] = {} + relocated: dict[str, str] = {} + references: list[tuple[tuple[str | int, ...], str]] = [] result = payload + + def writable_parent(path: tuple[str | int, ...]) -> dict[str, Any]: + nonlocal result + # Copy only the affected spine; never mutate caller-owned facts. + result = dict(result) + target: Any = result + for part in path[:-1]: + child = target[part] + target[part] = list(child) if isinstance(child, list) else dict(child) + target = target[part] + return cast(dict[str, Any], target) + for path in paths: parent: Any = result for part in path[:-1]: @@ -516,6 +530,9 @@ def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any] parent = None break field = str(path[-1]) + existing = parent.get(field + "_ref") if isinstance(parent, dict) else None + if isinstance(existing, str): + references.append((path, existing)) value = parent.get(field) if isinstance(parent, dict) else None if not isinstance(value, dict) or not value or field + "_ref" in parent: continue @@ -523,21 +540,23 @@ def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any] # the complete content, including revision, scope, failures and sources. encoded = json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False) reference = carriers.get(encoded) + own_ref = "$" + "".join( + f"[{part}]" if isinstance(part, int) else f".{part}" for part in path) if reference is None: - carriers[encoded] = "$" + "".join( - f"[{part}]" if isinstance(part, int) else f".{part}" for part in path) + carriers[encoded] = own_ref continue if len(encoded) <= len(reference) + 8: continue - # Copy only the affected spine, so callers retain their original facts. - result = dict(result) - target: Any = result - for part in path[:-1]: - child = target[part] - target[part] = list(child) if isinstance(child, list) else dict(child) - target = target[part] + target = writable_parent(path) del target[field] target[field + "_ref"] = reference + relocated[own_ref] = reference + # Retarget only references to owners actually moved by this projection. + # A later owner can move after an earlier alias was visited; a second pass + # keeps both directions direct without following arbitrary input chains. + for path, reference in references: + if reference in relocated: + writable_parent(path)[str(path[-1]) + "_ref"] = relocated[reference] return result diff --git a/tests/control_plane/test_quota_acceptance_references.py b/tests/control_plane/test_quota_acceptance_references.py index 24aefc5ddb..79f718c0c4 100644 --- a/tests/control_plane/test_quota_acceptance_references.py +++ b/tests/control_plane/test_quota_acceptance_references.py @@ -127,6 +127,66 @@ def test_reference_transport_keeps_distinct_original_numbers_and_later_exact_own assert json.dumps(project(result)) == json.dumps(result) +@pytest.mark.parametrize("existing_owner", ["planning", "replan"]) +def test_existing_reference_follows_its_relocated_complete_owner(existing_owner): + source = packet() + del source["agent_todo_summary"]["goal_acceptance_contract"] + owner = ("$.planning_horizon.core_goal.acceptance_contract" if existing_owner == "planning" + else "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract") + source["agent_todo_summary"]["goal_acceptance_contract_ref"] = owner + shared = _reference_inline_goal_acceptance(source) + assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == ( + "$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract") + envelope = build_turn_envelope(shared) + assert envelope["work_context"]["sources"][0]["content"]["goal_acceptance_contract"] == acceptance() + assert envelope["action_signature"]["matches"] is True + + +def test_reference_visited_before_its_owner_is_retargeted_after_sharing(): + source = packet() + core = source["planning_horizon"]["core_goal"] + del core["acceptance_contract"] + core["acceptance_contract_ref"] = "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract" + original = json.dumps(source) + shared = _reference_inline_goal_acceptance(source) + assert shared["planning_horizon"]["core_goal"]["acceptance_contract_ref"] == ( + "$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract") + assert shared["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["goal_acceptance_contract"] == acceptance() + assert json.dumps(source) == original + assert _reference_inline_goal_acceptance(shared) == shared + + +@pytest.mark.parametrize("reference", ["$.planning_horizon.core_goal.acceptance_contract", + "$.unknown.acceptance_contract"]) +def test_unmoved_or_unknown_reference_is_not_inferred_or_rewritten(reference): + source = packet(False) + del source["agent_todo_summary"]["goal_acceptance_contract"] + source["agent_todo_summary"]["goal_acceptance_contract_ref"] = reference + shared = _reference_inline_goal_acceptance(source) + assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == reference + assert shared["planning_horizon"]["core_goal"]["acceptance_contract"] == acceptance() + + +def test_shared_acceptance_survives_delta_and_lost_baseline_recovery(): + from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery + + initial = _reference_inline_goal_acceptance(packet()) + changed = packet() + changed["reason"] = "New feedback retains original requirements and publication stops." + current = _reference_inline_goal_acceptance(changed) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(initial, scope=scope, capture_id="one", surface="quota_packet") + full = prepare_context_delivery(current, scope=scope, capture_id="two", surface="quota_packet") + delta = prepare_context_delivery(current, scope=scope, capture_id="two", surface="quota_packet", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == "delta" + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == current + assert _reference_inline_goal_acceptance(restored) == current + assert restored["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["goal_acceptance_contract"] == acceptance() + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_real_cli_reuses_complete_acceptance_for_selection_and_replan(tmp_path, monkeypatch, provider): from test_replan_successor_guard_reentry import _fixture, AGENT, GOAL From 107336a53baec81812cd1173600b9b30f8f14047 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 30 Oct 2026 02:21:22 +0800 Subject: [PATCH 2/6] docs(controller): retain acceptance transport counterevidence Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 21 +++++++++++++++++++ ...ller-executor-unified-contract-v0.zh-CN.md | 15 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 57601013b0..d057ba5032 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -874,6 +874,27 @@ information and failure consequences. | Full/delta/resume delivery | Reuse the existing continuity owner after composition. Establish exact semantic reconstruction and required material availability before action; charge cold fallback and lost-baseline recovery. | | Scheduler, admission and settlement identities | One authoritative instance before capture; transport must preserve host ownership, original Turn and once-only effects. Repeated representations do not justify weakening their checks. | +A standalone late move of acceptance sharing is not automatically a useful +prerequisite. A controlled source-level substitution against main +`99cfa3ac559bfa237f04f36fb7e926672fea44b2` moved only that rule from the local +CLI compactor into the existing `reference_context` call. On the same synthetic +fixture with 100 distinct requirements, serialized projection input grew from +102,680 to 344,010 bytes; the 102,189-byte caller-visible output stayed equal. +The ordinary path still made one projection call; all-details delivery changed +from local sharing to one call. These are projection-parameter measurements, +not framed traffic, latency, model billing or the integrated C1 comparison. +Retain this negative result rather than declaring a net efficiency win. + +Repair relocated acceptance aliases at the current compatibility owner without +that extra transport. Existing aliases must follow only inline owners actually +moved by the projection, including aliases visited before their owner. Preserve +complete values, original numeric types, carrier order and existing size checks; +never infer arbitrary reference chains or rewrite saved captures. The complete +C1 composition must absorb this rule at its source-owning boundary and retire +the Python compatibility algorithm with its callers, rather than repeatedly +shipping all acceptance copies across another bridge. Writer/consumer cutover +and the original-quota outcome/full-cost gate remain open. + A fixed-source synthetic replan audit of candidate `895f38fc5` illustrates why the boundary matters. With stride off and no capture staging, the raw source was 21,461 compact UTF-8 JSON bytes, the original quota CLI projection 20,543, and diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 61e1518567..2746abce25 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -662,6 +662,21 @@ compactor:精确共享、省略和延后读取的信息与失败后果各不 | Full/delta/resume 交付 | 在 composition 后复用既有 continuity owner。行动前验证精确语义重建与必要材料可用;计入冷回退及丢失基线的恢复。 | | Scheduler、准入与结算身份 | Capture 前只有一份权威实例;传输保留真实 host owner、原 Turn 与一次性 effects。重复表示不构成削弱检查的理由。 | +单独后移验收共享不自动构成有价值的前置切片。以主线 +`99cfa3ac559bfa237f04f36fb7e926672fea44b2` 为基线的 source-level 受控替换, +只把该规则从本地 CLI compactor 移到已有 `reference_context` 调用。同一份含 100 条 +不同要求的合成 fixture 中,序列化投影输入从 102,680 增至 344,010 字节,调用者可见 +输出仍为相同的 102,189 字节。普通路径仍调用一次;全部 detail 开启时由本地共享变为 +一次请求。这些是投影参数测量,不是带协议包装的流量、延迟、模型计费或完整 C1 对照。 +保留此负结果,不能宣称净效率收益。 + +先在当前兼容 owner 修复被迁移的验收引用,不引入上述额外传输。已有别名只跟随本次 +投影实际迁移的内联 owner,包括先访问别名、后迁移 owner 的情况。保留完整值、原始 +数值类型、carrier 顺序和现有大小判断;不推断任意引用链,不重写已存 capture。 +完整 C1 composition 应在持有来源的边界吸收该规则,并随调用方退役 Python 兼容 +算法,不能把所有验收副本反复传过另一条 bridge。Writer/consumer 切换和原 quota +成果/完整成本门继续保持开放。 + 候选 `895f38fc5` 的固定 source 合成 replan 审计说明了边界的影响。关闭 stride 且不做 capture 分阶段时,原始 source 为 21,461 个紧凑 UTF-8 JSON 字节,原 quota CLI projection 为 20,543,完整候选为 21,738:**相对 CLI 增加 5.82%**,相对原始 source From 463074478d50fd5968d5a701fb4079b63b84bc69 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:46:53 +0800 Subject: [PATCH 3/6] perf(semantics): reuse parsed field presence for production scans Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/semantics/python_production.py | 40 +++++++++++++------ .../architecture/test_semantic_production.py | 35 ++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/loopx/semantics/python_production.py b/loopx/semantics/python_production.py index 7ddeb26b5d..9f5dab3466 100644 --- a/loopx/semantics/python_production.py +++ b/loopx/semantics/python_production.py @@ -9,6 +9,7 @@ import ast from collections import Counter from dataclasses import dataclass +from functools import lru_cache from types import SimpleNamespace from typing import Mapping, TypeVar @@ -137,20 +138,29 @@ def enum_members(source: SourceFile, symbol: str, *, strict: bool = False) -> di return result +@lru_cache(maxsize=4096) +def _field_mentions(source: SourceFile) -> frozenset[str]: + """Reuse parsed names, not another retained AST or a source-text filter.""" + names: set[str] = set() + for node in ast.walk(_parsed(source)): + if isinstance(node, ast.Name): + names.add(node.id) + elif isinstance(node, ast.Attribute): + names.add(node.attr) + elif isinstance(node, ast.Constant) and isinstance(node.value, str): + names.add(node.value) + elif isinstance(node, ast.keyword) and node.arg is not None: + names.add(node.arg) + return frozenset(names) + + def python_literal_uses(source: SourceFile, field: str) -> set[str]: """Literal writes and direct dispatch operands; not alias/data-flow proof.""" - tree = _parsed(source) - nodes = list(ast.walk(tree)) - # Inspect parsed names/keys, including escaped strings and normalized - # identifiers. Parse errors must still fail even when the field is absent. - if not any( - isinstance(node, ast.Name) and node.id == field - or isinstance(node, ast.Attribute) and node.attr == field - or isinstance(node, ast.Constant) and node.value == field - or isinstance(node, ast.keyword) and node.arg == field - for node in nodes - ): + # Parsing precedes absence: escaped keys and normalized identifiers count, + # and invalid syntax must fail even without a textual field mention. + if field not in _field_mentions(source): return set() + tree = _parsed(source) def reads(node: ast.AST) -> bool: if isinstance(node, ast.Name): @@ -188,7 +198,7 @@ def literals(node: ast.AST) -> set[str]: # conditions and does not inspect strings containing sample source text. rows = scan_python_production(source, field=field, enums={}) result = set().union(*(row.values for row in rows)) - for node in nodes: + for node in ast.walk(tree): if isinstance(node, ast.Compare): for left, operator, right in zip( [node.left, *node.comparators[:-1]], node.ops, node.comparators, strict=True, @@ -407,6 +417,12 @@ def scan_python_production( call_arguments = call_arguments or {} calls = _qualified_bindings(source, tree, call_arguments, modules) return_paths = return_paths or {} + # Every output form needs a field, registered return or qualified owner. + # Keep enum results and builder arguments even when the field is absent. + if not bindings and not calls and not return_functions and ( + not field or field not in _field_mentions(source) + ): + return [] module_functions = _module_functions(source, tree) call_memo: dict[tuple[str, str], tuple[frozenset[str], tuple[str, ...]]] = {} environments: dict[tuple[int, str], SimpleNamespace] = {} diff --git a/tests/architecture/test_semantic_production.py b/tests/architecture/test_semantic_production.py index 6cda493d43..6d9e11dedd 100644 --- a/tests/architecture/test_semantic_production.py +++ b/tests/architecture/test_semantic_production.py @@ -28,6 +28,41 @@ def test_literal_scan_rejects_invalid_syntax_without_a_field_mention(): source = SourceFile('loopx/broken_probe.py', '.py', 'def broken(') with pytest.raises(SyntaxError): python_production.python_literal_uses(source, 'action') + with pytest.raises(SyntaxError): + python_production.scan_python_production(source, field='action', enums={}) + + +@pytest.mark.parametrize('text,options,expected', [ + ("def choose():\n return 'run'", {'return_functions': frozenset({'choose'})}, + {('return', frozenset({'run'}), False)}), + ("class Action:\n RUN='run'\ndef choose():\n return Action.RUN", + {'enums': {'loopx/field_absence.py::Action': {'RUN': 'run'}}}, + {('enum_result', frozenset({'run'}), False)}), + ("def pack(result):\n return result\npack('run')", + {'call_arguments': {'loopx/field_absence.py::pack': {'result': 0}}}, + {('call_argument', frozenset({'run'}), False)}), + ("def choose(value):\n return value", {'return_functions': frozenset({'choose'})}, + {('return', frozenset(), True)}), +]) +def test_field_absence_preserves_registered_and_enum_evidence(text, options, expected): + source = SourceFile('loopx/field_absence.py', '.py', text) + assert 'action' not in text + rows = python_production.scan_python_production(source, **{'field': 'action', 'enums': {}, **options}) + assert {(row.form, row.values, row.unresolved) for row in rows} == expected + + +def test_field_presence_tracks_source_changes_despite_hash_collisions(): + class CollidingText(str): + def __hash__(self): + return 0 + + for text, expected in [ + ("other = 'wait'", set()), + (r"packet['\x61ction'] = 'run'", {'run'}), + ("other = 'run'", set()), + ]: + source = SourceFile('loopx/field_presence.py', '.py', CollidingText(text)) + assert python_production.python_literal_uses(source, 'action') == expected @pytest.mark.parametrize('first_text,second_text', [ From 70d206cabefc2a0a985ce6ea01ba874b11dbbfc0 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:46:54 +0800 Subject: [PATCH 4/6] docs(self-repair): record repeated static scan diagnosis Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- skills/loopx-self-repair/references/repair-patterns.md | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 88cc509b8f..e6dc0d70a6 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -70,6 +70,7 @@ teaches a reusable control-plane lesson. | `admission_timer_reentry_gap` | A positively admitted Todo with an unchanged, already-due timer immediately becomes writeback-only on same-Turn reentry before producing a result. | The original positive admission receipt, its retained resume/readiness facts and timestamp, current canonical Todo, exact binding, and File/SQLite replay. | Recovery classified every external wait as introduced after admission; today's due flag alone cannot establish when a wait was authored. | Freeze scalar resume/readiness facts at the first positive admission and compare them in the existing typed wait owner with microsecond timestamps. Continue only the unchanged runnable timer; later waits and missing/malformed legacy facts retain original closeout. Preserve identity, clocks, gates and one debit. Never clear a legitimate wait, rewrite receipts, fabricate progress or reset a retry clock to escape recovery. | | `blocked_turn_retry_history_gap` | An exact blocked Turn can close without spending, but its open Todo is immediately selected again or loses its wait when other runs crowd the status display; a peer hard lease prevents the blocked agent from updating the Todo. | Exact Goal/Agent/Todo/Turn receipt, peer lease, canonical Todo validator, persisted retry due time, full run index versus compact recent runs and cached index digest, next-Turn selection and spend count. | The settlement had no bounded retry authority, or quota selection reconstructed it from truncated or stale status projection and tried to mutate a peer-gated Todo. | Persist one bounded retry on the blocked Turn receipt, retain it in the full-index semantic projection, and fence cached scheduler status against the current Run index before projecting it for the same agent. Keep the canonical Todo and terminal validator intact; prove replay, no spend, crowded history, stale cache, and real File/SQLite hard-lease readback. | | `host_closeout_history_scan_amplification` | Quota entry approaches or exceeds its runtime timeout only on a long-lived Goal, especially when many prior Turns already have valid closeouts. | Turn and receipt counts, rollout-log and runs-index sizes, candidate count, per-stage elapsed time, declared runtime budget, and the same synthetic settled history before and after repair. | Preflight rebuilt append-order recency with repeated list searches, then each candidate reread, reparsed, and rescanned the complete event and run history. A larger timeout hid the multiplicative work without bounding it. | Parse each persisted source once, retain strict-versus-tolerant read semantics in the shared snapshot, build append-order and exact Turn/effect indexes, and reduce every candidate from that snapshot. Restore the ordinary runtime budget and cover a sufficiently large all-settled history that must traverse every candidate; never truncate history, skip identity validation, or raise the timeout as the repair. | +| `static_scan_repeats_module_analysis` | Required vocabulary or static checks approach their timeout as tracked modules and vocabulary queries grow. | Exact source/runtime, unchanged tracked-source population, complete production and unknown observations, phase CPU/elapsed time, native budget and memory footprint; escaped/normalized-name, invalid-syntax and source-change counterexamples. | Each field query rebuilds module/scope analysis even without a parsed field, registered return or qualified enum/builder. A source-substring shortcut would lose escaped strings and normalized identifiers. | Reuse bounded immutable parsed-name facts keyed by the complete source beside the existing analyzer. Parse before proving absence; retain enum, return, builder and unknown evidence. Compare complete observations and CPU, preserve hash-collision/source-change behavior, and rerun the full native check under its original budget. Retain failed or contended runs and disclose memory costs. | | `turn_replay_recovery_semantic_gap` | A real Turn resumes from a safe Journal prefix, but `inspect-journal` presents `replay_legal=false` as if recovery were forbidden; scheduler-only or saved-Host-result recovery is especially misleading. | Journal integrity fields, replay decision, executor-adopted recovery decision, completed phase prefix, complete typed settlement identity, authoritative selected-Todo lineage, conditional Host Session Binding check, prepared-effect presence, and bounded recovery outcome. | Effect-free terminal replay and effectful executor recovery were collapsed into one user-facing decision even though the executor used separate status/phase rules; initial repairs also omitted either the settlement identity or its binding-to-Turn cross-check, allowing a drifted goal, agent, or canonical Todo to reach a later effect. | Keep replay legality and Journal consistency distinct. Make one typed recovery decision the source used by both executor and inspection; validate and cross-bind the canonical settlement goal, agent, Turn instance, binding, and effect id before authorization, including the selected Todo or adaptive primary Todo; project continue/resume phase/Host reinvocation/reason plus only participating checks, and persist a bounded planned-versus-actual audit. Cover identity and binding drift with zero-provider-call regressions. Preserve the existing prepared-effect readback owner and do not claim general exactly-once semantics. | | `managed_chat_resume_turn_identity_gap` | A managed Chat Session resumes after its adapter becomes unhealthy, returns to `ready`, and clears `active_turn_id`, but the interrupted Turn remains nonterminal and unowned. | Pre-resume Session snapshot, post-prepare persisted Session, adapter health, interrupted Turn status and error code, final Session state. | Resume preparation cleared the persisted active Turn reference before adapter recovery, then recovery re-read only the mutated Session and lost the pre-resume Turn identity. | Carry the pre-mutation Turn id through the fresh closed-state check, terminalize that Turn as `failed/server_restarted` before restoring the Session, and cover unhealthy-adapter resume with a focused regression while retaining the per-Session lifecycle lock. | | `dashboard_chat_stdio_encoding_gap` | Dashboard Chat completes ASCII turns but Codex app-server closes on non-ASCII input on Windows; direct stderr reports invalid UTF-8. | Failing multilingual request, app-server stderr, Python locale encoding, app-server `Popen` text-pipe options, and the same request under an explicit UTF-8 control. | The text-mode stdio pipe inherited the Windows system code page even though the app-server JSON-RPC transport requires UTF-8. | Set `encoding="utf-8"` explicitly on the app-server subprocess pipes and retain a focused launch-contract regression so host locale cannot change the protocol encoding. | From 64a60ba185895d022c5d1b9c933c67cee6cb70c1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:01:37 +0800 Subject: [PATCH 5/6] refactor(semantics): clarify scanner expression types Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/semantics/python_production.py | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/loopx/semantics/python_production.py b/loopx/semantics/python_production.py index 9f5dab3466..fef8df7267 100644 --- a/loopx/semantics/python_production.py +++ b/loopx/semantics/python_production.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from functools import lru_cache from types import SimpleNamespace -from typing import Mapping, TypeVar +from typing import Mapping, TypeVar, cast from .inventory import SourceFile @@ -232,9 +232,9 @@ def _qualified_bindings(source: SourceFile, tree: ast.Module, owners: Mapping[st if value is None and modules is not None and alias.name in symbols: # One unrenamed re-export hop through a tracked module binds the # same owner; only names that are owner symbols are followed. - target = _tracked_module(module, modules) - if target is not None and target.path != source.path: - value = _reexported(target, alias.name, imports) + reexport_source = _tracked_module(module, modules) + if reexport_source is not None and reexport_source.path != source.path: + value = _reexported(reexport_source, alias.name, imports) if value is not None: bindings[name] = value else: @@ -333,11 +333,11 @@ def _module_functions(source: SourceFile, tree: ast.Module) -> dict[str, ast.Fun # name some scope declares ``global`` and then stores is no longer reliably # the ``def`` above; binding a call to that ``def`` would report the original # body's returns for a function the module can swap at runtime. - for node in ast.walk(tree): - if not isinstance(node, ast.Global): + for global_node in ast.walk(tree): + if not isinstance(global_node, ast.Global): continue - scope = _enclosing_scope(tree, node) - for name in node.names: + scope = _enclosing_scope(tree, global_node) + for name in global_node.names: if name in defined and _stores_name(scope, name): bound[name] += 1 functions = {name: node for name, node in defined.items() @@ -348,14 +348,14 @@ def _module_functions(source: SourceFile, tree: ast.Module) -> dict[str, ast.Fun def _index_value(node: ast.AST) -> str | int | None: if isinstance(node, ast.Constant) and type(node.value) in (str, int): - return node.value + return cast(str | int, node.value) if (isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Constant) and type(node.operand.value) is int): return -node.operand.value return None -def _union(values: list[ast.AST]) -> ast.AST: +def _union(values: list[ast.expr]) -> ast.expr: """Fold several definitions of one local into a finite selection node. The scan reports syntactic result possibilities, so a name written more than @@ -497,7 +497,7 @@ def collect(node: ast.AST, loops: frozenset[int] = frozenset()) -> None: if scope == '': nested_names -= local_owner_names | {owner.split('::')[1] for owner in call_arguments if owner.split('::')[0] == source.path} - import_bound = set() + import_bound: set[str] = set() for node in nodes: if isinstance(node, (ast.Import, ast.ImportFrom)): import_bound.update(alias.asname or alias.name.split('.')[0] for alias in node.names) @@ -526,7 +526,7 @@ def collect(node: ast.AST, loops: frozenset[int] = frozenset()) -> None: local_bindings = {k: v for k, v in bindings.items() if k not in shadows} local_calls = {k: v for k, v in calls.items() if k not in shadows} - plain: dict[str, list[ast.AST]] = {} + plain: dict[str, list[ast.expr]] = {} for node in nodes: target = value = None if (isinstance(node, ast.Assign) and len(node.targets) == 1 @@ -536,7 +536,7 @@ def collect(node: ast.AST, loops: frozenset[int] = frozenset()) -> None: and node.value is not None): target, value = node.target.id, node.value if target is not None: - plain.setdefault(target, []).append(value) + plain.setdefault(target, []).append(cast(ast.expr, value)) declared = {name for node in nodes if isinstance(node, (ast.Global, ast.Nonlocal)) for name in node.names} # Every store of the name must be one of those plain writes, so a value @@ -788,7 +788,7 @@ def same_module_call(node: ast.Call, mode: str) -> tuple[set[str], bool] | None: blocked(reason) return set(values), bool(reasons) - def enum_object_value(node: ast.AST, seen: frozenset[str]) -> tuple[set[str], bool]: + def enum_object_value(node: ast.AST | None, seen: frozenset[str]) -> tuple[set[str], bool]: node, seen = bound(node, seen) if isinstance(node, ast.IfExp): parts = [enum_object_value(value, seen) for value in (node.body, node.orelse)] @@ -871,7 +871,7 @@ def scan_scope(body: list[ast.stmt], scope: str, parameters: set[str], env = env if env is not None else environment(body, scope, parameters, call_shadows, own) blockers = env.blockers - def record(node: ast.AST | None, form: str, location: ast.AST) -> None: + def record(node: ast.AST | None, form: str, location: ast.expr | ast.stmt) -> None: # Reasons are read back by position: one scope's environment is # shared with same-module call resolution, which may nest inside. start = len(blockers) From 1dc6cb85d1203eeea1236d70c3127782fd375379 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:33:23 +0800 Subject: [PATCH 6/6] fix(semantics): retain parsed module result type Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/semantics/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopx/semantics/inventory.py b/loopx/semantics/inventory.py index 008bf836a0..c341a3e056 100644 --- a/loopx/semantics/inventory.py +++ b/loopx/semantics/inventory.py @@ -116,7 +116,7 @@ def _assigned_name(node: ast.AST) -> tuple[str, ast.AST] | None: return None -def parse_python(source: SourceFile) -> ast.AST: +def parse_python(source: SourceFile) -> ast.Module: """Parse one tracked Python source, rejecting an unparseable one by path. Deliberately not cached. Caching the trees would hold roughly two million