From 79c8cf80e290fc3bf5fd8614b5e6566dfaf00efc Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:02:17 +0800 Subject: [PATCH] perf(portfolio): share executable revision across one read Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 10 ++++- ...script-control-plane-migration-v0.zh-CN.md | 9 ++++- loopx/goal_portfolio.py | 2 + tests/test_goal_portfolio.py | 39 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 44368ff77e..1d0c882a52 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -4,7 +4,7 @@ - Supersedes / closes: none - Proposed by: LoopX maintainers - Date: 2026-08-15 -- Last revised: 2026-10-20 +- Last revised: 2026-10-21 - Scope: an incremental, replacement-first migration of the LoopX control-plane core from Python to TypeScript without maintaining two semantic implementations @@ -36,6 +36,14 @@ delivery. Fewer requests alone do not establish a performance gain. ### Product conversations and asynchronous inbox +**Bounded Goal portfolio reads.** `goal_portfolio.py` now joins the existing +executable-revision request scope around the complete portfolio read. CLI, +Chat context and manager inspection callers reuse that scope across status and +per-Agent quota reads. Nested scopes join it; the next portfolio read resolves +the current executable again. Registry, provider and run-history reads remain +live, with their existing scope, limits and change detection. This is bounded +caller adoption: no new bridge, data cache, semantic owner or provider routing. + **Receiver Inbox read transaction (2026-10-19).** `loopx/control_plane/collaboration/inbox_read.ts` now composes local source scans, pagination, Goal-instance admission, receiver and peer-return read receipts, and unfinished diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index 3dee7c57d9..ac64a9a837 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -4,7 +4,7 @@ - 替代 / 关闭:无 - Proposed by:LoopX maintainers - Date:2026-08-15 -- Last revised:2026-10-20 +- Last revised:2026-10-21 - Scope:LoopX 控制面核心从 Python 到 TypeScript 的增量、replacement-first 迁移;不长期维护两份语义实现 - Tracking issue:[#3225](https://github.com/loopx-project/loopx/issues/3225) @@ -34,6 +34,13 @@ SQLite CLI 性能。仅减少请求次数不能证明性能收益。 ### 产品对话与通用异步 inbox +**有界 Goal portfolio 读取。** `goal_portfolio.py` 现在在完整 portfolio 读取期间 +复用既有 executable-revision 请求作用域。CLI、Chat context 和 manager inspection +调用者在 status 与逐 Agent quota 读取之间共用该作用域。嵌套作用域加入当前请求; +下一次 portfolio 读取重新解析当前代码。Registry、provider 与 run-history 仍实时 +读取,保留既有范围、上限和变化检查。这是有界调用者接入,不新增桥接、数据缓存、 +语义 owner 或 provider 路由。 + **接收方 Inbox 读取事务(2026-10-19)。** `loopx/control_plane/collaboration/inbox_read.ts` 现在通过既有 typed owner 组合本地源扫描、分页、 Goal-instance 准入、接收方及 peer-return 读取回执和未完成委派上下文。 diff --git a/loopx/goal_portfolio.py b/loopx/goal_portfolio.py index b6be257efc..b7695076ef 100644 --- a/loopx/goal_portfolio.py +++ b/loopx/goal_portfolio.py @@ -12,6 +12,7 @@ from typing import Any from .agent_registry import registered_agent_ids_for_goal +from .control_plane.effect_runtime import effect_runtime_request_scope from .control_plane.goals.activation import goal_activation_state from .control_plane.goals.goal_ref_validation import exact_goal_ref from .control_plane.quota.should_run import build_quota_should_run @@ -327,6 +328,7 @@ def _read_goal( return row +@effect_runtime_request_scope() def build_goal_portfolio( *, registry_path: Path, diff --git a/tests/test_goal_portfolio.py b/tests/test_goal_portfolio.py index abf3486fcf..8898e7b93c 100644 --- a/tests/test_goal_portfolio.py +++ b/tests/test_goal_portfolio.py @@ -516,3 +516,42 @@ def read(goal, **kwargs): assert result['coverage']['stopped_excluded'] == 0 assert result['goals'][0]['activation_state'] == 'unknown' assert result['goals'][0]['quality'] == 'conflicting' + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_portfolio_resolves_code_once_per_read_and_keeps_registration_live( + tmp_path, monkeypatch, provider, +): + from tests.control_plane.canonical_authority_fixture import ( + isolate_sqlite_runtime, promoted_create_fixture, + ) + from loopx.control_plane import effect_runtime + + isolate_sqlite_runtime(tmp_path, monkeypatch) + path, _, state = promoted_create_fixture( + tmp_path, provider=provider, registered_agents=["agent-a", "agent-b"], + ) + source = json.loads(path.read_text()) + source["goals"][0].update(domain="portfolio", adapter={"kind": "generic_project_goal_v0"}) + path.write_text(json.dumps(source)) + state_before = state.read_bytes() + fingerprints = [] + fingerprint = effect_runtime._runtime_fingerprint + + def observe_code_read(): + revision = fingerprint() + fingerprints.append(revision) + return revision + + monkeypatch.setattr(effect_runtime, "_runtime_fingerprint", observe_code_read) + first = portfolio.build_goal_portfolio(registry_path=path) + assert len(fingerprints) == 1, f"resolved code {len(fingerprints)} times" + assert [a["agent_id"] for a in first["goals"][0]["agents"]] == ["agent-a", "agent-b"] + source["goals"][0]["coordination"]["registered_agents"] = ["agent-b"] + path.write_text(json.dumps(source)) + second = portfolio.build_goal_portfolio(registry_path=path) + assert len(fingerprints) == 2 + assert [a["agent_id"] for a in second["goals"][0]["agents"]] == ["agent-b"] + assert second["inventory_revision"] != first["inventory_revision"] + assert second["goals"][0]["progress"] == "unknown" + assert state.read_bytes() == state_before