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
24 changes: 24 additions & 0 deletions docs/architecture/rfcs/monorepo-distribution-split-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,30 @@ Approval needs a reviewed decision, not implementation progress or silence.
it does not complete R0's full V10 census, M1 public-import migration, split
packaging or R1 managed minimal. D1–D7 remain open. No new preset or default.

### 2026-11-03 — M1 public status compatibility characterization

- **Boundary:** `ChatStatusRequestMixin` and `goal_read_runtime_root` still live
in `loopx.chat_status_api`. The Chat HTTP handler and Todo detail are active
consumers. This is a characterization prerequisite, not a relocated owner.
- **Compatibility evidence:** `tests/test_chat_status_public_compatibility.py`
protects historical imports in three fresh-process import orders, historical
pickle lookups, reload with already-imported references, and live registry,
collector and host-observation patch points through the real HTTP handler.
Discovery tolerates a missing registry, not unreadable or invalid metadata.
A copied-export facade mutation fails the source-bound Todo-consumer patch
oracle: re-exported functions alone do not preserve legacy module globals.
- **Next reviewed slice:** move the complete status owner under `loopx/chat/`
with an identity-compatible public bridge and documented I3 deprecation.
Retarget both the exact loader path and `_discovery_registry` exception in
`tests/architecture/test_source_session_registry_denial.py` together, retaining
metadata-only discovery and executable source-session refusal. The present
slice leaves that owner and oracle unchanged; do not split ownership or relax
the denial oracle just to make a partial move pass.
- **Remaining:** status relocation and its installed-wheel integration are not
delivered by characterization. I3 retains at least one minor release; D3's
exact expiry remains a release-owner decision. M1 is open. No module-count,
loading-performance, split-package or R1 managed-Turn benefit is claimed.

## Appendix B: Decision log

| Date | Decision | Owner / approval | Alternatives | Normative sections changed |
Expand Down
19 changes: 19 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 @@ -551,6 +551,25 @@ R0 可以先于目录移动交付,但不代表内核合格。R3/R4 可选,
M1 公开 import 迁移、发行物拆分或 R1 managed minimal。D1–D7 仍未决,
不新增预设或改变默认值。

### 2026-11-03 — M1 公开 status 兼容性刻画

- **边界:** `ChatStatusRequestMixin` 与 `goal_read_runtime_root` 仍由
`loopx.chat_status_api` 所有,Chat HTTP handler 与 Todo detail 是活跃调用方。
本片交付刻画前置条件,不是 owner 迁移。
- **兼容证据:** `tests/test_chat_status_public_compatibility.py` 保护三个新进程
import 顺序下的历史 import、历史 pickle 查找、reload 后已导入引用,以及经真实
HTTP handler 生效的 registry、collector 和宿主观察 patch 点。发现路径仅容忍
registry 缺失,不吞掉不可读或非法元数据。复制导出的 facade mutation 被源绑定
Todo 调用方的 patch oracle 拒绝:仅 re-export 函数不能保留旧模块的 globals。
- **下一完整评审片:** 将完整 status owner 迁入 `loopx/chat/`,保留模块身份兼容的
公开 bridge,并文档化 I3 弃用。同步重定向
`tests/architecture/test_source_session_registry_denial.py` 中精确 loader 路径与
`_discovery_registry` 函数例外,保留仅元数据发现及可执行 source-session 拒绝。
本片不改变该 owner 或 oracle;不能为通过部分迁移而拆散所有权或放松拒绝 oracle。
- **剩余:** 刻画不交付 status 迁移及其安装 wheel 集成。I3 至少保留一个 minor
版本,D3 精确退役时间仍由发布负责人决定。M1 未关闭,不声明模块数量、加载性能、
拆包或 R1 managed Turn 收益。

## 附录 B:决策日志

| 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 |
Expand Down
148 changes: 148 additions & 0 deletions tests/test_chat_status_public_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""I3 protects historical imports and their live patch points, not copied exports."""

from __future__ import annotations

import http.client
import importlib
import json
import subprocess
import sys
import threading
from types import SimpleNamespace

import pytest

from loopx.chat_server import ChatHTTPServer, ChatRequestHandler


@pytest.mark.parametrize("first_import", [
"loopx.chat_status_api", "loopx.chat_server", "loopx.chat.todo_detail",
])
def test_historical_status_imports_survive_import_order_reload_and_pickle(first_import):
# A fresh process avoids an earlier test silently choosing the import order.
code = """
import importlib
import pickle
import sys
from types import SimpleNamespace

importlib.import_module(sys.argv[1])
public = importlib.import_module('loopx.chat_status_api')
from loopx.chat_status_api import ChatStatusRequestMixin, goal_read_runtime_root
from loopx.chat_server import ChatRequestHandler
from loopx.chat.todo_detail import goal_read_runtime_root as todo_runtime_root

assert issubclass(ChatRequestHandler, ChatStatusRequestMixin)
assert todo_runtime_root is goal_read_runtime_root
assert pickle.loads(b'cloopx.chat_status_api\\nChatStatusRequestMixin\\n.') is ChatStatusRequestMixin
assert pickle.loads(b'cloopx.chat_status_api\\ngoal_read_runtime_root\\n.') is goal_read_runtime_root

# Reload replaces definitions, but already-imported functions keep live globals.
assert importlib.reload(public) is public
assert importlib.import_module('loopx.chat_status_api') is public
public.load_project_registry = lambda _: {'common_runtime_root': 'source-owned'}
server = SimpleNamespace(registry_path=None, runtime_root_override='chat-decoy')
assert goal_read_runtime_root(server) is None
assert todo_runtime_root(server) is None
"""
result = subprocess.run(
[sys.executable, "-c", code, first_import],
capture_output=True, text=True, timeout=60, check=False,
)
assert result.returncode == 0, result.stderr


@pytest.mark.parametrize("source_bound,controller_root,expected", [
(True, "coordinator-decoy", None),
(False, "controller-runtime", "controller-runtime"),
(False, None, "chat-fallback"),
])
def test_historical_registry_patch_reaches_already_imported_todo_consumer(
monkeypatch, tmp_path, source_bound, controller_root, expected,
):
from loopx.chat.todo_detail import goal_read_runtime_root

registry_path = tmp_path / "registry.json"
calls = []

def registry(path):
calls.append(path)
return {"common_runtime_root": "source-runtime"} if source_bound else {}

monkeypatch.setattr("loopx.chat_status_api.load_project_registry", registry)
server = SimpleNamespace(
registry_path=registry_path,
runtime_root_override="chat-fallback",
runtime_controller=SimpleNamespace(coordination_runtime_root=controller_root),
)
assert goal_read_runtime_root(server) == expected
assert calls == [registry_path]


@pytest.mark.parametrize("failure", [FileNotFoundError, PermissionError, ValueError])
def test_historical_discovery_patch_only_tolerates_missing_registry(monkeypatch, failure):
public = importlib.import_module("loopx.chat_status_api")

def unreadable(_path):
raise failure("synthetic unavailable registry")

monkeypatch.setattr("loopx.chat_status_api.load_project_registry", unreadable)
server = SimpleNamespace(registry_path=None, runtime_root_override=None)
if failure is FileNotFoundError:
assert public.goal_read_runtime_root(server) is None
else:
with pytest.raises(failure):
public.goal_read_runtime_root(server)


def test_historical_status_patch_points_reach_the_real_http_handler(monkeypatch, tmp_path):
registry = {"goals": [{"id": "alpha"}]}
registry_reads, collected = [], []

def load_registry(path):
registry_reads.append(path)
return registry

def collect_status(**kwargs):
collected.append(kwargs)
return {"ok": True, "compatibility_probe": "collector"}

def attach_activity(projection, **_kwargs):
projection["compatibility_activity"] = True

def attach_delivery(projection, **_kwargs):
projection["compatibility_delivery"] = True

monkeypatch.setattr("loopx.chat_status_api.load_registry", load_registry)
monkeypatch.setattr("loopx.chat_status_api.load_project_registry", lambda _: {})
monkeypatch.setattr("loopx.chat_status_api.collect_status", collect_status)
monkeypatch.setattr("loopx.chat_status_api.attach_host_thread_activity", attach_activity)
monkeypatch.setattr("loopx.chat_status_api.attach_host_delivery_windows", attach_delivery)
monkeypatch.setattr("loopx.chat_status_api.codex_thread_observers", lambda **_: [])
server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
server.registry_path = tmp_path / "registry.json"
server.runtime_root = tmp_path / "runtime"
server.runtime_root_override = None
server.selected_goal_id = None
server.scan_roots = []
server.limit = 20
server.verbose = False
server.goal_subagent_configuration_enabled = False
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
connection = http.client.HTTPConnection(*server.server_address, timeout=5)
try:
connection.request("GET", "/status.json?goal_id=alpha")
response = connection.getresponse()
payload = json.loads(response.read())
assert response.status == 200, payload
assert payload["compatibility_probe"] == "collector"
assert payload["compatibility_activity"] is True
assert payload["compatibility_delivery"] is True
assert len(collected) == 1 and collected[0]["goal_id"] == "alpha"
assert registry_reads == [server.registry_path, server.registry_path]
finally:
connection.close()
server.shutdown()
thread.join(timeout=5)
server.server_close()
Loading