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 1/4] 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 2/4] 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 3/4] 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 4/4] 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