Skip to content
Open
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
11 changes: 11 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 恢复与旧代码删除推进。
Expand Down
4 changes: 4 additions & 0 deletions loopx/capabilities/manager_context/README.md
Original file line number Diff line number Diff line change
@@ -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
`<runtime-root>/.local/manager-context/policy.json`:
Expand Down
15 changes: 1 addition & 14 deletions loopx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions loopx/cli_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
47 changes: 46 additions & 1 deletion tests/test_cli_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"],
Expand All @@ -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,
Expand Down
Loading