Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,57 @@ Approval needs a reviewed decision, not implementation progress or silence.
installation, R1 managed minimal or D1–D7. No preset, frontend operation,
permission or persisted state changes; protected execution keeps its owner.

### 2026-11-06 — History reader selected loading

- **Baseline and boundary:** `22b0f6c25`. Ordinary `history` reads and help use
the existing registrar/handler through the shared CLI grammar and guard.
Maintenance actions, rejected parser requests and a preloaded full CLI retain
the original route, including handler/renderer patch points. History algorithms,
evidence windows, public imports and the TS owner remain unchanged. This is a
reversible R0 reader slice in the monolithic distribution; I3 still requires
at least one minor release for public moves.
- **Real-path evidence:** 27 actual-console cases per baseline/candidate source
and installed-wheel environment preserve stdout/stderr/exit codes, UTC order,
Agent filtering, limits, empty/unknown inputs and current/stale evidence reads.
Four additional installed/source facade probes preserve patched handlers and
renderers. The preloaded-handler oracle failed before its correction; final
history/parser selection passes 26 tests. Existing durable history, artifact
and evidence oracles are retained. Required native services use disposable
process and data namespaces, with authenticated exit readback.
- **Matched measurement:** Apple M4 Pro, 48 GiB, macOS 27 arm64, Python 3.13.5,
Node 26.0.0; matched Python dependencies and source-pair Node dependencies.
Each of four workloads has 30 interleaved baseline/candidate pairs, precompiled
bytecode and fresh Python processes with warm native-service reuse. The final
sampling takes 229 seconds; all three rounds total 698 seconds, below the
frozen 20-minute sampling bound. Setup, validation and tracing are separate.
Earlier environment/refinement observations retain their original scope.

| Workload | Median / p95 elapsed ms, base → candidate | Median CPU ms | Median CLI peak RSS MiB |
| --- | --- | --- | --- |
| Source help | 1552 / 1862 → 204 / 349 | 670 → 100 | 91.4 → 31.3 |
| Source Agent read | 1603 / 1833 → 417 / 482 | 665 → 150 | 91.9 → 34.4 |
| Wheel help | 1621 / 2053 → 220 / 375 | 695 → 100 | 91.8 → 31.0 |
| Wheel Agent read | 1648 / 1825 → 414 / 462 | 725 → 150 | 92.2 → 34.6 |

- **Loading and effects:** source/wheel help loads 155 rather than 1157 LoopX
modules; registered reads load 208 rather than 1158, evidence reads 210 rather
than 1159. Python audit read-open observations decrease from 1290 to 246 for
source help and 1311 to 723 for its cold native read; wheel observations are
1291 → 247 and 1710 → 723. Help has zero observed writes/processes/connections.
Cold registered reads retain six write-opens, two process starts and one
connection, with 252/355 client IPC bytes sent/received. Evidence reads retain
one write-open, two connections and 1377/804 IPC bytes. These required effects
are not optional activation. A serving runtime can work without Node on PATH;
cold absence retains the existing failure. Output bytes match. Timing includes
interpreter and process invocation; descendant CPU/RSS/I/O, full model and
attention costs remain unknown.
- **Holds and remaining work:** measured help p95 remains above the proposed
200 ms target; D6 is not adopted. Source-session loader inventory and Turn
preview import checks fail identically on the untouched baseline. Their failures
are retained as qualification holds. This does not close full R0/V10, M1,
split installation, R1/context/host qualification or open design decisions.
Revert the loader slice for rollback; no state migration is involved.

### 2026-11-06 — Selected version loading and matched costs

The independent proposal `8a79d8602e699e51452ca1f43dc9b9527c81aa1b` at baseline
Expand Down
40 changes: 40 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,46 @@ R0 可以先于目录移动交付,但不代表内核合格。R3/R4 可选,
不关闭完整 R0/V10、M1、拆包安装、R1 managed minimal 或 D1–D7。不改变 preset、
前端操作、权限或持久化状态;受保护执行仍由既有 owner 所有。

### 2026-11-06 — History 读取路径的所选加载

- **基线与边界:** `22b0f6c25`。普通 `history` 读取与帮助通过共享 CLI grammar
和 guard 复用既有 registrar/handler。维护动作、parser 拒绝及已预加载完整 CLI
的进程保留原路径,包括 handler/renderer patch 点。历史算法、证据窗口、公开
import 与 TS owner 均不变。这是单体发行物中可回滚的 R0 读取片;公开迁移的
I3 仍至少保留一个 minor 版本。
- **真实路径证据:** 基线/候选源码与实际安装 wheel 的四个环境,各有 27 个
console 用例保留 stdout/stderr/退出码、UTC 排序、Agent 过滤、limit、空/未知
输入与当前/过期证据读取。另有四个安装/源码 facade 探针保留被 patch 的
handler 与 renderer。预加载 handler 的 oracle 在修复前失败;最终 history/
parser 选择通过 26 个测试。既有历史、artifact 与证据 oracle 保留。必需原生
服务使用可丢弃的数据/进程命名空间,并通过认证的退出回读确认清理。
- **匹配测量:** Apple M4 Pro、48 GiB、macOS 27 arm64、Python 3.13.5、
Node 26.0.0;Python 依赖及源码配对中的 Node 依赖版本一致。四个负载各有 30
组交错基线/候选配对,bytecode 预编译,Python 每次启动新进程,原生服务暖复用。
最终采样 229 秒,三轮合计 698 秒,低于冻结的 20 分钟采样上限;安装、验证及
tracing 单独记录。此前环境/兼容性修正前的观察保留原有效性范围。

| 负载 | elapsed 中位数 / p95 ms,基线 → 候选 | CPU 中位数 ms | CLI 峰值 RSS 中位数 MiB |
| --- | --- | --- | --- |
| 源码帮助 | 1552 / 1862 → 204 / 349 | 670 → 100 | 91.4 → 31.3 |
| 源码 Agent 读取 | 1603 / 1833 → 417 / 482 | 665 → 150 | 91.9 → 34.4 |
| Wheel 帮助 | 1621 / 2053 → 220 / 375 | 695 → 100 | 91.8 → 31.0 |
| Wheel Agent 读取 | 1648 / 1825 → 414 / 462 | 725 → 150 | 92.2 → 34.6 |

- **加载与效果:** 源码/wheel 帮助由 1157 降至 155 个 LoopX 模块,注册 Goal
读取由 1158 降至 208,证据读取由 1159 降至 210。Python audit 的 read-open
观察中,源码帮助为 1290 → 246,冷原生读取为 1311 → 723;wheel 分别为
1291 → 247、1710 → 723。帮助观察到的写入/进程/连接均为零。冷注册读取
保留六次 write-open、两个进程启动、一个连接,客户端 IPC 发送/接收为
252/355 bytes;证据读取保留一次 write-open、两个连接及 1377/804 IPC bytes。
这些必需效果不属于可选激活。已有服务可在 PATH 无 Node 时继续工作,冷缺失
保留原拒绝。输出字节一致;耗时包含解释器与进程调用,子孙进程 CPU/RSS/I/O、
完整模型与人工注意力成本仍未知。
- **保留项与后续:** 实测帮助 p95 仍高于提出的 200 ms 目标,不采纳 D6。
source-session loader 清单及 Turn preview import 检查在未改动基线上同样失败,
保留为资格验证限制。本片不关闭完整 R0/V10、M1、拆包安装、R1/context/宿主
资格或开放设计决策。回滚只需撤回加载片,不涉及状态迁移。

### 2026-11-06 — Version 按需加载及匹配成本

独立候选 `8a79d8602e699e51452ca1f43dc9b9527c81aa1b` 对比基线
Expand Down
24 changes: 24 additions & 0 deletions examples/cli-control-plane-command-modularization-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,35 @@ def assert_todo_markdown_error_payload() -> None:
assert_contains(result.stdout, "- error: todo add requires --role", "todo markdown error")


def assert_history_reader_route() -> None:
with tempfile.TemporaryDirectory(prefix="loopx-cli-history-smoke-") as tmp:
script = f"""
import contextlib
import io
import json
import sys
from loopx.entrypoint import main
output = io.StringIO()
with contextlib.redirect_stdout(output):
result = main(["--registry", {str(Path(tmp) / 'absent.json')!r},
"--format", "json", "history", "--agent-id", "fixture-agent"])
assert result == 1
assert json.loads(output.getvalue()) == {{"ok": False,
"error": "--agent-id requires --goal-id without a history action"}}
for module in ("loopx.cli", "loopx.chat_server", "loopx.cli_commands.benchmark_dispatch"):
assert module not in sys.modules, module
"""
result = subprocess.run([sys.executable, "-c", script], cwd=REPO_ROOT,
text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stdout + result.stderr


def main() -> int:
assert_source_shape()
assert_help_surfaces()
assert_todo_error_payload()
assert_todo_markdown_error_payload()
assert_history_reader_route()
print("cli-control-plane-command-modularization-smoke: ok")
return 0

Expand Down
19 changes: 19 additions & 0 deletions loopx/cli_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"goal-acceptance",
"host-mode-plan",
"capability",
"history",
}
# Existing CLI vocabulary; this set chooses loading, not operation authority.
_CAPABILITY_DISCOVERY_COMMANDS = frozenset({"list", "show"})
Expand Down Expand Up @@ -499,6 +500,10 @@ def _build_selected_parser(command: str) -> LoopXArgumentParser:
from .cli_commands.capability import register_capability_commands

register_capability_commands(subparsers, add_subcommand_format)
elif command == "history":
from .cli_commands.history import register_history_command

register_history_command(subparsers)
else: # pragma: no cover - caller guards the private interface
raise ValueError(f"unsupported selected command: {command}")
return parser
Expand Down Expand Up @@ -827,6 +832,14 @@ def _dispatch_selected(args: argparse.Namespace, raw_argv: list[str]) -> int:
registry_was_supplied=user_supplied_registry(raw_argv),
output_format=output_format, print_payload=print_payload,
)
elif args.command == "history":
from .cli_commands.history import handle_history_command

# Direct full-CLI callers retain their original facade-bound handler.
result = handle_history_command(
args, registry_path=registry_path, runtime_root_arg=args.runtime_root,
print_payload=print_payload,
)
else:
result = dispatch_common_command(
args,
Expand Down Expand Up @@ -885,11 +898,17 @@ def _run_command(command: str | None, raw_argv: list[str]) -> int:
or "register_version_command" in vars(version_exports))
):
return _run_full_cli(raw_argv)
if command == "history" and "loopx.cli" in sys.modules:
# An explicitly loaded facade keeps its existing handler/renderer bindings.
return _run_full_cli(raw_argv)
if command in _SELECTED_COMMANDS:
try:
args = _build_selected_parser(command).parse_args(raw_argv)
except _SelectedParseRejected:
return _run_full_cli(raw_argv)
if command == "history" and args.history_action is not None:
# Select ordinary reads; maintenance keeps its complete original route.
return _run_full_cli(raw_argv)
if command == "capability":
if args.capability_command not in _CAPABILITY_DISCOVERY_COMMANDS:
return _run_full_cli(raw_argv)
Expand Down
181 changes: 181 additions & 0 deletions tests/test_cli_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1857,3 +1857,184 @@ def test_global_read_preserves_empty_coverage_without_loading_full_cli(tmp_path,
assert payload["coverage"]["complete"] is False
assert payload["goals"] == []
assert registry.read_bytes() == before


@pytest.mark.parametrize("arguments", [
["--help"], ["--evidence-ref", "absent", "--help"],
["repair-index-duplicates", "--help"],
])
def test_history_help_loads_only_its_reader(arguments):
result = run_cli_main("loopx.entrypoint", ["history", *arguments],
forbidden_modules=("loopx.cli", "loopx.chat_server",
"loopx.cli_commands.benchmark_dispatch"),
environment={"PATH": ""})
assert result.returncode == 0, result.stderr
assert "--evidence-ref" in result.stdout
assert "--agent-id" in result.stdout
assert result.stderr == ""


@pytest.mark.parametrize("format_name", ["json", "markdown"])
@pytest.mark.parametrize("limit", [-1, 0, 1, 10])
def test_history_reader_preserves_scope_chronology_and_limits(tmp_path, format_name, limit):
registry, runtime = write_command_fixture(tmp_path)
index = runtime / "goals/perf-goal/runs/index.jsonl"
index.parent.mkdir(parents=True)
rows = [
{"goal_id": "perf-goal", "agent_id": "perf-agent",
"generated_at": "2026-08-19T00:30:00+08:00", "classification": "offset-earlier"},
{"goal_id": "perf-goal", "agent_id": "perf-agent",
"generated_at": "2026-08-18T17:00:00Z", "classification": "utc-later"},
{"goal_id": "perf-goal", "agent_id": "other-agent",
"generated_at": "2026-08-18T18:00:00Z", "classification": "other-lane"},
]
index.write_text("".join(json.dumps(row) + "\n" for row in rows))
before = {p: p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()}
argv = ["--registry=" + str(registry), "--runtime-root=" + str(runtime),
"--cli-bin", "loopx", "--format", format_name, "history",
"--goal-id", "perf-goal", "--agent-id", "perf-agent", "--limit", str(limit)]
selected = run_cli_main("loopx.entrypoint", argv, forbidden_modules=("loopx.cli",))
full = run_cli_main("loopx.cli", argv)
assert selected.returncode == full.returncode == 0, (selected.stderr, full.stderr)
assert selected.stdout == full.stdout
assert selected.stderr == full.stderr == ""
assert "other-lane" not in selected.stdout
if format_name == "json":
payload = json.loads(selected.stdout)
expected = ["utc-later", "offset-earlier"][:max(0, limit)]
assert [row["classification"] for row in payload["runs"]] == expected
assert [row["classification"] for row in payload["goals"][0]["latest_runs"]] == expected
assert {p: p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()} == before


def test_history_reader_retains_required_native_runtime_failure(tmp_path):
registry, runtime = write_command_fixture(tmp_path)
index = runtime / "goals/perf-goal/runs/index.jsonl"
index.parent.mkdir(parents=True)
index.write_text(json.dumps({"goal_id": "perf-goal", "agent_id": "perf-agent",
"generated_at": "2026-08-18T17:00:00Z", "classification": "bounded_probe"}) + "\n")
argv = ["--registry", str(registry), "--runtime-root", str(runtime),
"--format", "json", "history", "--goal-id", "perf-goal"]
temporary = tmp_path / "no-node-runtime"
temporary.mkdir()
environment = {"PATH": "", **{key: str(temporary) for key in ("TMPDIR", "TEMP", "TMP")}}
selected = run_cli_main("loopx.entrypoint", argv, forbidden_modules=("loopx.cli",),
environment=environment)
full = run_cli_main("loopx.cli", argv, environment=environment)
assert selected.returncode == full.returncode == 1
assert selected.stdout == full.stdout
assert selected.stderr == full.stderr == ""
assert json.loads(selected.stdout)["error"] == "LoopX Effect runtime requires Node.js 22.22.3 or newer on PATH"


@pytest.mark.parametrize("case", ["current", "other-agent", "stale", "no-node"])
def test_history_reader_preserves_native_evidence_reference_window(tmp_path, case):
from loopx.control_plane.work_items.replan_context_codec import project_replan_context

registry, runtime = write_command_fixture(tmp_path)
index = runtime / "goals/perf-goal/runs/index.jsonl"
index.parent.mkdir(parents=True)
observation = "Keep independent failure evidence.\nPreserve the next decision's constraint."
index.write_text(json.dumps({"goal_id": "perf-goal", "agent_id": "perf-agent",
"generated_at": "2026-08-18T17:00:00Z", "classification": "bounded_probe",
"health_check": observation}) + "\n")
context = project_replan_context(goal_id="perf-goal", agent_id="perf-agent", runs=(),
source_status={"registry": str(registry), "runtime_root": str(runtime),
"run_history": {"goals": []}})
argv = ["--registry", str(registry), "--runtime-root", str(runtime),
"--format", "json", "history", "--goal-id", "perf-goal", "--agent-id",
"other-agent" if case == "other-agent" else "perf-agent",
"--evidence-ref", context["evidence"][0]["evidence_ref"], "--limit", "0"]
if case == "stale":
index.write_text("")
before = index.read_bytes()
environment = None
if case == "no-node":
temporary = tmp_path / "no-node-runtime"
temporary.mkdir()
environment = {"PATH": "", **{key: str(temporary) for key in ("TMPDIR", "TEMP", "TMP")}}
selected = run_cli_main("loopx.entrypoint", argv, forbidden_modules=("loopx.cli",),
environment=environment)
full = run_cli_main("loopx.cli", argv, environment=environment)
assert selected.returncode == full.returncode == (0 if case == "current" else 1)
assert selected.stdout == full.stdout
assert selected.stderr == full.stderr == ""
payload = json.loads(selected.stdout)
if case == "current":
assert payload["evidence"]["health_check"] == observation
elif case == "no-node":
assert "requires Node.js" in payload["error"]
else:
assert "unavailable" in payload["error"]
assert index.read_bytes() == before


def test_history_reader_keeps_complete_parser_diagnostics():
cases = [
["history", "unknown-action"], ["history", "--unknown"],
["history", "--agent-i", "agent"], ["history", "--evidence-ref"],
["history", "--limit", "invalid"], ["history", "--format", "json"],
["--format", "unknown", "history"], ["--cli-bin", "", "history"],
["--registry", "history"], ["--runtime-root", "history"],
]
selected, full = run_cli_batch("loopx.entrypoint", cases), run_cli_batch("loopx.cli", cases)
assert selected == full
assert all(result["returncode"] == 2 and not result["stdout"] for result in selected)


@pytest.mark.parametrize("arguments,error", [
(["--agent-id", "perf-agent"], "--agent-id requires --goal-id without a history action"),
(["--evidence-ref", "absent", "--goal-id", "perf-goal"],
"--evidence-ref requires --goal-id and --agent-id without a history action"),
])
def test_history_reader_validation_refusals_match_full_cli(tmp_path, arguments, error):
argv = ["--registry", str(tmp_path / "missing.json"), "--format", "json", "history", *arguments]
selected = run_cli_main("loopx.entrypoint", argv, forbidden_modules=("loopx.cli",))
full = run_cli_main("loopx.cli", argv)
assert selected.returncode == full.returncode == 1
assert selected.stdout == full.stdout
assert selected.stderr == full.stderr == ""
assert json.loads(selected.stdout) == {"ok": False, "error": error}
assert not list(tmp_path.iterdir())


@pytest.mark.parametrize("action", [
None, "inspect-index-duplicates", "repair-index-duplicates",
"rebuild-index-collisions", "trajectory-hygiene",
])
def test_history_maintenance_and_explicit_full_cli_keep_patch_points(tmp_path, action):
arguments = ["history", *([action] if action else []), "--goal-id", "perf-goal"]
# Direct full-CLI bindings retain their handler even for ordinary reads.
module = "loopx.entrypoint" if action else "loopx.cli"
script = f"""
import loopx.cli as full
from {module} import main
seen = []
def original_path(args, **kwargs):
seen.append((args.history_action, args.goal_id))
return 73
full.handle_history_command = original_path
assert main(["--registry", {str(tmp_path / 'absent.json')!r}, *{arguments!r}]) == 73
assert seen == [({action!r}, "perf-goal")]
"""
result = run_isolated_script(script)
assert result.returncode == 0, result.stderr
assert not list(tmp_path.iterdir())


def test_history_entrypoint_retains_preloaded_full_cli_handler(tmp_path):
script = f"""
import loopx.cli as full
from loopx.entrypoint import main
seen = []
def original_path(args, **kwargs):
seen.append((args.history_action, args.goal_id))
return 73
full.handle_history_command = original_path
assert main(["--registry", {str(tmp_path / 'absent.json')!r},
"history", "--goal-id", "perf-goal"]) == 73
assert seen == [(None, "perf-goal")]
"""
result = run_isolated_script(script)
assert result.returncode == 0, result.stderr
assert not list(tmp_path.iterdir())
Loading