From d941ee98c599e6edf5a006d14e6cbe8e07010b86 Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:57:07 +0800 Subject: [PATCH] perf(cli): reuse the shared Inbox command dispatcher Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- .../typescript-control-plane-migration-v0.md | 11 +++++ ...script-control-plane-migration-v0.zh-CN.md | 8 ++++ loopx/capabilities/manager_context/README.md | 4 ++ loopx/cli.py | 15 +----- loopx/cli_runtime.py | 13 +++++ tests/test_cli_entrypoint.py | 47 ++++++++++++++++++- 6 files changed, 83 insertions(+), 15 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..ce31008576 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -55,6 +55,17 @@ HTTP caller parity and complete default-SQLite CLI performance remain separate qualification gates. This source cutover does not certify R1–R3 or the complete asynchronous collaboration lifecycle. +**Inbox CLI caller adoption.** The normal `manager-inbox` command family now +uses the existing selected-command parser, shared dispatcher and executable- +revision request scope. The full compatibility CLI reuses that same dispatcher; +its duplicate Inbox branch is retired. Both paths retain the existing parser, +handler, registry/runtime resolution and native-controller guard. A command +resolves executable identity once while source records stay live reads under +their existing locks; the next command resolves the current code again. +Existing TypeScript semantic owners, bridge operations, receipt formats and +Python provider IO are unchanged. This is Stage 3 caller adoption, not another +leaf migration, a new result store or completion of R1–R3. + R1–R3 also consume the typed kernel through the App, not only CLI settlement. The [App/inbox integration plan](app-conversation-and-async-inbox-v0.md#ts-and-generic-async-inbox-migrate-with-the-user-path) orders caller fixes, characterization, one complete async lifecycle cutover, 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..0c7cfc1a68 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 @@ -49,6 +49,14 @@ finalize 重新取得 Goal lifetime guard,并在共享 request fence 内重读 完整默认 SQLite CLI 性能仍是独立资格门。本次源码切换不证明 R1–R3 或完整异步协作 生命周期已通过验收。 +**Inbox CLI 调用者接入。** 正常 `manager-inbox` 命令族现在使用既有按命令 +parser、共享 dispatcher 和 executable-revision 请求作用域。完整兼容 CLI +复用同一个 dispatcher,旧 Inbox 重复分支已退役。两条路径保留既有 parser、 +handler、registry/runtime 解析和 native-controller guard。一次命令只解析一次 +可执行代码身份;源记录仍在原锁协议下实时读取,下一次命令重新解析当前代码。 +既有 TypeScript 语义 owner、桥接操作、回执格式和 Python provider IO 均不变。 +这是 Stage 3 调用者接入,不是新的叶子迁移、结果存储,也不证明 R1–R3 已完成。 + R1–R3 的 TS 消费者包括 App 产品路径,不只 CLI 结算。 [App/inbox 融合设计](app-conversation-and-async-inbox-v0.md#ts-and-generic-async-inbox-migrate-with-the-user-path) 按调用者修复、行为刻画、完整异步生命周期切换、真实 adapter 恢复与旧代码删除推进。 diff --git a/loopx/capabilities/manager_context/README.md b/loopx/capabilities/manager_context/README.md index dd5a359fd6..a3471ff47d 100644 --- a/loopx/capabilities/manager_context/README.md +++ b/loopx/capabilities/manager_context/README.md @@ -1,6 +1,10 @@ # Manager context delivery Built-in capability for original intent delivery and receiver-owned replanning. +The `manager-inbox` CLI uses the shared selected-command dispatcher and one +executable-revision scope per command. It retains the existing handler and +live source reads; it does not load the full compatibility CLI or cache request +records between commands. The owner's local manager channel uses registered workers automatically. An external conversation requires a trusted operator's sender-bound policy in `/.local/manager-context/policy.json`: diff --git a/loopx/cli.py b/loopx/cli.py index dbad49fa7b..13677ccc80 100644 --- a/loopx/cli.py +++ b/loopx/cli.py @@ -23,7 +23,7 @@ register_native_child_commands, handle_native_child_command, ) from .cli_commands.todo_continuation import register_todo_continuation, handle_todo_continuation -from .cli_commands.manager_inbox import register_manager_inbox, handle_manager_inbox +from .cli_commands.manager_inbox import register_manager_inbox from .cli_commands.delegation import register_delegation from .capabilities.content_ops.cli import ( handle_content_ops_command, @@ -813,19 +813,6 @@ def main(argv: list[str] | None = None) -> int: print_payload, output_format, ) - if args.command == "manager-inbox": - from .control_plane.projects.registry_codec import load_project_registry - from .paths import resolve_runtime_root - - return handle_manager_inbox( - args, - registry_path, - resolve_runtime_root( - load_project_registry(registry_path), - args.runtime_root, - registry_path=registry_path, - ), - ) lark_inbox_result = handle_lark_inbox_command( args, registry_path=registry_path, diff --git a/loopx/cli_runtime.py b/loopx/cli_runtime.py index 2058cda57c..ff195321fb 100644 --- a/loopx/cli_runtime.py +++ b/loopx/cli_runtime.py @@ -68,6 +68,7 @@ _SELECTED_COMMANDS = _STATUS_COMMANDS | { "todo", "quota", "task-lease", "change-window", "delegation", "turn", "doctor", "commands", "authority-archive", "coordination-shadow", "extension", "slash-commands", + "manager-inbox", } @@ -286,6 +287,10 @@ def _build_selected_parser(command: str) -> LoopXArgumentParser: from .cli_commands.turn_registration import register_turn_commands register_turn_commands(subparsers, add_subcommand_format) + elif command == "manager-inbox": + from .cli_commands.manager_inbox import register_manager_inbox + + register_manager_inbox(subparsers, add_subcommand_format) elif command == "authority-archive": from .cli_commands.authority_archive import register_authority_archive_command @@ -321,6 +326,14 @@ def _dispatch_common_command( registry_path: Path, allow_missing_registry: bool, ) -> int | None: + if args.command == "manager-inbox": + from .cli_commands.manager_inbox import handle_manager_inbox + from .control_plane.projects.registry_codec import load_project_registry + from .paths import resolve_runtime_root + + return handle_manager_inbox(args, registry_path, resolve_runtime_root( + load_project_registry(registry_path), args.runtime_root, registry_path=registry_path, + )) if args.command == "task-lease": from .cli_commands.task_lease import handle_task_lease_command diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index 7d11afb5ac..b9398adbe0 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -75,7 +75,7 @@ def run_cli_main( except SystemExit as exc: exit_code = exc.code for forbidden_module in {forbidden_modules!r}: - assert forbidden_module not in sys.modules + assert forbidden_module not in sys.modules, "unexpected module import: " + forbidden_module raise SystemExit(exit_code) """ return run_isolated_script(script, environment=environment) @@ -442,6 +442,11 @@ def test_selected_parser_matches_full_help_and_diagnostics() -> None: ["task-lease", "renew", "--goal-id", "goal", "--todo-id", "todo", "--expected-ver", "1"], ["task-lease", "renew", "--goal-id", "goal", "--todo-id", "todo", "--ttl-seconds", "not-a-number"], ["delegation", "--help"], + ["manager-inbox", "--help"], + ["manager-inbox", "unknown-action"], + ["manager-inbox", "report", "--unknown-option"], + ["manager-inbox", "report", "--request-revision", "not-an-integer"], + ["manager-inbox", "acknowledge", "--decision", "unknown-decision"], ["turn", "--help"], ["turn", "run-once", "--help"], ["status", "--unknown-option"], @@ -458,6 +463,46 @@ def test_selected_parser_matches_full_help_and_diagnostics() -> None: assert selected == full +@pytest.mark.parametrize("arguments", [ + ["acknowledge", "--request-id", "a" * 64, "--decision", "adopt", "--reason", "Checked the source"], + ["report", "--request-id", "a" * 64, "--reply-text", "The evidence check passed."], +]) +def test_selected_manager_inbox_refusals_match_without_loading_full_cli(tmp_path, arguments): + registry, runtime_root = write_command_fixture(tmp_path) + argv = ["--registry", str(registry), "--runtime-root", str(runtime_root), "--format", "json", + "manager-inbox", *arguments, "--goal-id", "perf-goal", "--agent-id", "perf-agent"] + 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"] is False + + +def test_selected_manager_result_matches_full_replay_without_loading_full_cli(tmp_path): + from test_collaboration_goal_instance import _create_source_registry, _manager_request + from loopx.control_plane.collaboration.inbox import _root, acknowledge + + registry = _create_source_registry(tmp_path) + _, _, receipt = _manager_request(tmp_path, registry) + rid = receipt["request_id"] + acknowledge(tmp_path, "delivery", "builder", rid, "adopt", "Checked the source", + registry=registry, caller_goal_ref=receipt["goal_ref"]) + argv = ["--registry", str(registry), "--runtime-root", str(tmp_path), "--format", "json", + "manager-inbox", "report", "--goal-id", "delivery", "--agent-id", "builder", + "--request-id", rid, "--reply-text", "The independent evidence check passed."] + selected = run_cli_main("loopx.entrypoint", argv, forbidden_modules=("loopx.cli",)) + path = _root(tmp_path) / "replies" / rid / "conclusion.json" + assert selected.returncode == 0, selected.stderr + committed = path.read_bytes() + full = run_cli_main("loopx.cli", argv) + assert full.returncode == 0, full.stderr + assert selected.stdout == full.stdout + assert selected.stderr == full.stderr + assert json.loads(selected.stdout)["delivered"] is False + assert path.read_bytes() == committed + + @pytest.mark.parametrize("action", ["acquire", "renew", "transfer", "release", "inspect"]) def test_selected_task_lease_dispatch_preserves_owner_arguments( tmp_path: Path, action: str,