From c079c895ea75e54e329efa8471b59b4118d9b1f5 Mon Sep 17 00:00:00 2001 From: xiaobaifly7 <196511160+xiaobaifly7@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:11:01 +0800 Subject: [PATCH] feat(scheduler): support Codex App native Goal runtime profile Signed-off-by: xiaobaifly7 <196511160+xiaobaifly7@users.noreply.github.com> --- docs/heartbeat-automation-prompt.md | 23 +++++++++++++ loopx/cli_commands/quota_context.py | 2 +- loopx/control_plane/heartbeat/host.py | 2 +- loopx/control_plane/quota/spend_sources.py | 5 ++- .../runtime/usage_statistics_goal_contract.ts | 2 +- .../scheduler/execution_context.py | 20 +++++++++-- .../turn_driver/codex_context.py | 19 +++++++---- loopx/project_prompt.py | 19 ++++++----- .../references/heartbeat-hosts.md | 31 +++++++++++++++++ .../test_context_machine_configuration.py | 2 +- tests/control_plane/test_heartbeat_cli.py | 22 +++++++++++++ .../test_quota_settlement_cli.py | 33 ++++++++++++++----- .../test_scheduler_execution_context.py | 8 +++++ .../usage_statistics_goals.test.ts | 6 ++++ tests/test_host_loop_activation.py | 7 ++-- .../test_visible_goal_terminal_settlement.py | 7 ++-- 16 files changed, 173 insertions(+), 35 deletions(-) diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index b36e22985a..e8c09e3730 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -64,6 +64,29 @@ automation contract. ### Native Goal bootstrap and live execution instructions +For an already connected local Codex App conversation whose user requests native +Goal continuation, select `codex_app_goal` explicitly: + +```bash +loopx --format json --registry heartbeat-prompt \ + --bootstrap --thin --runtime-profile codex_app_goal \ + --goal-id --agent-id +``` + +Use the successful returned `task_body` with the current App's native Goal +controls, preserving the existing objective and registered identity. The App +owns continuation; this route neither starts another Codex process nor creates +an automation. Existing blocked Goals require explicit host resume rather than +replacement. The runtime context is `codex_app` / `goal_runtime` / `interactive`; +quota settlement uses `visible-goal`. Follow the live guard's `--begin-turn` +re-entry when requested instead of assigning a synthetic native turn ID. + +This is opt-in: `--codex-app` still means `codex_app_heartbeat`, and ordinary +`codex-app` onboarding keeps its existing automation behavior. A committed +LoopX receipt is not evidence of a native Goal trigger. Qualification must pair +the actual host turn with admitted work, writeback and settlement, then observe +native continuation in the same conversation. + Full, compact and brief automation use the same fully qualified notification/execution rule as thin, including `heartbeat_recommendation.agent_must_attempt` and `execution_obligation.must_attempt_work`. None embeds a second diff --git a/loopx/cli_commands/quota_context.py b/loopx/cli_commands/quota_context.py index bb4213b48a..b4e0794cc6 100644 --- a/loopx/cli_commands/quota_context.py +++ b/loopx/cli_commands/quota_context.py @@ -243,7 +243,7 @@ def validate_quota_command_context_request( if profile not in GUIDED_START_TURN_RUNTIME_PROFILES: raise QuotaCommandValidationError( "--begin-turn requires runtime-profile codex_app_heartbeat, " - "trae_app, or codex_app_ssh_goal; every other host starts its turn by " + "trae_app, codex_app_goal, or codex_app_ssh_goal; every other host starts its turn by " "passing its own --turn-instance-id" ) if ( diff --git a/loopx/control_plane/heartbeat/host.py b/loopx/control_plane/heartbeat/host.py index 78796997bf..92e909d2b6 100644 --- a/loopx/control_plane/heartbeat/host.py +++ b/loopx/control_plane/heartbeat/host.py @@ -81,7 +81,7 @@ def uses_native_goal_host_loop( context = resolution.context if context.execution_mode is not ExecutionMode.INTERACTIVE: return False - if context.host_surface is HostSurface.ARK_MANAGED_AGENT: + if context.host_surface in {HostSurface.ARK_MANAGED_AGENT, HostSurface.CODEX_APP}: return context.scheduler_owner is SchedulerOwner.GOAL_RUNTIME return ( context.host_surface in {HostSurface.CODEX_APP_SSH, HostSurface.CODEX_CLI} diff --git a/loopx/control_plane/quota/spend_sources.py b/loopx/control_plane/quota/spend_sources.py index fbea522c56..178335107f 100644 --- a/loopx/control_plane/quota/spend_sources.py +++ b/loopx/control_plane/quota/spend_sources.py @@ -76,7 +76,10 @@ def host_goal_turn_reentry_action( and settlement_plan is None and turn_instance_id is None ): - if profile is SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE: + if profile in { + SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE, + SchedulerRuntimeProfile.CODEX_APP_NATIVE_GOAL, + }: return f"{typed_quota_guard} --begin-turn" # CLI/managed Goal hosts own their iteration identity, not an App # heartbeat receipt. Re-enter before exposing any unbound settlement. diff --git a/loopx/control_plane/runtime/usage_statistics_goal_contract.ts b/loopx/control_plane/runtime/usage_statistics_goal_contract.ts index 08e4450d34..d90d0440b2 100644 --- a/loopx/control_plane/runtime/usage_statistics_goal_contract.ts +++ b/loopx/control_plane/runtime/usage_statistics_goal_contract.ts @@ -10,7 +10,7 @@ export type Measurement = typeof MEASUREMENTS[number]; export type Host = typeof HOSTS[number]; export function hostCategory(value: unknown): Host { if ((HOSTS as readonly unknown[]).includes(value)) return value as Host; - const aliases: Record = { "codex-app": "codex_app", "codex-app-ssh": "codex_app", codex_app: "codex_app", codex_app_heartbeat: "codex_app", codex_app_ssh_goal: "codex_app", "codex-cli": "codex_cli", "codex-cli-tui": "codex_cli", codex_cli: "codex_cli", codex: "codex_cli", "codex-ide-plugin": "codex_cli", "deepseek-harness-native": "dsh", "claude-code": "claude_code", claude_code: "claude_code", dsh: "dsh", opencode: "opencode", trae_app: "trae", "kiro-cli": "kiro_cli", "generic-cli": "other", generic_cli: "other" }; + const aliases: Record = { "codex-app": "codex_app", "codex-app-ssh": "codex_app", codex_app: "codex_app", codex_app_heartbeat: "codex_app", codex_app_goal: "codex_app", codex_app_ssh_goal: "codex_app", "codex-cli": "codex_cli", "codex-cli-tui": "codex_cli", codex_cli: "codex_cli", codex: "codex_cli", "codex-ide-plugin": "codex_cli", "deepseek-harness-native": "dsh", "claude-code": "claude_code", claude_code: "claude_code", dsh: "dsh", opencode: "opencode", trae_app: "trae", "kiro-cli": "kiro_cli", "generic-cli": "other", generic_cli: "other" }; return typeof value === "string" && Object.hasOwn(aliases, value) ? aliases[value] : "unknown"; } export type GoalCount = { measurement: Measurement; host: Host; span: GoalDuration; duration: GoalDuration; count: number }; diff --git a/loopx/control_plane/scheduler/execution_context.py b/loopx/control_plane/scheduler/execution_context.py index 54b0b30420..6c9c68f2e2 100644 --- a/loopx/control_plane/scheduler/execution_context.py +++ b/loopx/control_plane/scheduler/execution_context.py @@ -41,6 +41,7 @@ class ExecutionMode(str, Enum): class SchedulerRuntimeProfile(str, Enum): ARK_MANAGED_AGENT_GOAL = "ark_managed_agent_goal" CODEX_APP_HEARTBEAT = "codex_app_heartbeat" + CODEX_APP_NATIVE_GOAL = "codex_app_goal" CODEX_APP_SSH_VISIBLE = "codex_app_ssh_goal" CODEX_CLI_VISIBLE = "codex_cli" TRAE_APP = "trae_app" @@ -59,6 +60,7 @@ class SchedulerRuntimeProfile(str, Enum): NATIVE_GOAL_RUNTIME_PROFILES = frozenset( { SchedulerRuntimeProfile.ARK_MANAGED_AGENT_GOAL, + SchedulerRuntimeProfile.CODEX_APP_NATIVE_GOAL, SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE, SchedulerRuntimeProfile.CODEX_CLI_VISIBLE, } @@ -83,6 +85,7 @@ class SchedulerRuntimeProfile(str, Enum): GUIDED_START_TURN_RUNTIME_PROFILES = frozenset( { SchedulerRuntimeProfile.CODEX_APP_HEARTBEAT, + SchedulerRuntimeProfile.CODEX_APP_NATIVE_GOAL, SchedulerRuntimeProfile.TRAE_APP, SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE, } @@ -112,6 +115,11 @@ class SchedulerRuntimeProfile(str, Enum): SchedulerOwner.HOST_AUTOMATION, ExecutionMode.HOSTED_AUTOMATION, ), + SchedulerRuntimeProfile.CODEX_APP_NATIVE_GOAL: ( + HostSurface.CODEX_APP, + SchedulerOwner.GOAL_RUNTIME, + ExecutionMode.INTERACTIVE, + ), SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE: ( HostSurface.CODEX_APP_SSH, SchedulerOwner.AGENT_CLI_LOOP, @@ -257,7 +265,10 @@ def _validation_errors(context: SchedulerExecutionContext) -> list[str]: HostSurface.KUNLUNCODE, HostSurface.KIRO_CLI, } - if context.host_surface in {HostSurface.CODEX_APP, HostSurface.TRAE_APP}: + if context.host_surface in {HostSurface.CODEX_APP, HostSurface.TRAE_APP} and not ( + context.host_surface is HostSurface.CODEX_APP + and context.scheduler_owner is SchedulerOwner.GOAL_RUNTIME + ): host_name = context.host_surface.value if context.scheduler_owner is not SchedulerOwner.HOST_AUTOMATION: errors.append(f"{host_name} requires scheduler_owner=host_automation") @@ -299,8 +310,11 @@ def _validation_errors(context: SchedulerExecutionContext) -> list[str]: if context.execution_mode is ExecutionMode.HOSTED_AUTOMATION: errors.append("agent_cli_loop cannot use execution_mode=hosted_automation") if context.scheduler_owner is SchedulerOwner.GOAL_RUNTIME: - if context.host_surface is not HostSurface.ARK_MANAGED_AGENT: - errors.append("goal_runtime requires host_surface=ark_managed_agent") + if context.host_surface not in { + HostSurface.ARK_MANAGED_AGENT, + HostSurface.CODEX_APP, + }: + errors.append("goal_runtime requires host_surface=ark_managed_agent or codex_app") if context.execution_mode is not ExecutionMode.INTERACTIVE: errors.append("goal_runtime requires execution_mode=interactive") if ( diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index cef3964ab8..f2548165e4 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -75,14 +75,21 @@ def context_delivery_activation(setting: str, *, runtime_profile: str | None = N resolution = resolve_scheduler_execution_context(scheduler_execution_context) context = resolution.context if resolution.ok else None if context is not None: - # The machine's application row selects Codex. Other explicitly - # resolved hosts keep full delivery; an explicit CLI on still + # The machine preference leaves unsupported declared modes on + # full delivery; an explicit CLI on still # rejects them. Unknown/conflicting declarations never bypass this # adapter, and generic_cli still needs the benchmark declaration. - if codex_machine_preference and context.host_surface in { - HostSurface.ARK_MANAGED_AGENT, HostSurface.TRAE_APP, HostSurface.CLAUDE_CODE, - HostSurface.KUNLUNCODE, HostSurface.KIRO_CLI, HostSurface.LOCAL_SCHEDULER, - }: + if codex_machine_preference and ( + context.host_surface in { + HostSurface.ARK_MANAGED_AGENT, HostSurface.TRAE_APP, HostSurface.CLAUDE_CODE, + HostSurface.KUNLUNCODE, HostSurface.KIRO_CLI, HostSurface.LOCAL_SCHEDULER, + } + or ( + context.host_surface is HostSurface.CODEX_APP + and context.scheduler_owner is SchedulerOwner.GOAL_RUNTIME + and context.execution_mode is ExecutionMode.INTERACTIVE + ) + ): return context_delivery_activation("off") if (context.host_surface is HostSurface.CODEX_APP and context.scheduler_owner is SchedulerOwner.HOST_AUTOMATION diff --git a/loopx/project_prompt.py b/loopx/project_prompt.py index 026f00b135..e236c58d2d 100644 --- a/loopx/project_prompt.py +++ b/loopx/project_prompt.py @@ -18,8 +18,10 @@ from .control_plane.progress_scope import AGENT_LANE_PROGRESS_SCOPE from .control_plane.scheduler.execution_context import ( GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, - SchedulerRuntimeProfile, + GUIDED_START_TURN_RUNTIME_PROFILES, + NATIVE_GOAL_RUNTIME_PROFILES, render_scheduler_execution_args, + scheduler_execution_context_for_runtime_profile, scheduler_runtime_profile_for_execution_context, ) from .control_plane.todos.contract import normalize_required_capabilities @@ -185,15 +187,14 @@ def render_quota_guard_command( begin_turn: bool = False, include_shared_registry: bool = True, ) -> str: + profile = scheduler_runtime_profile_for_execution_context( + scheduler_execution_context_for_runtime_profile(runtime_profile) + if runtime_profile is not None + else scheduler_execution_context + ) if not heartbeat_turn_receipt and agent_id and ( - runtime_profile == SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE.value - or ( - runtime_profile is None - and scheduler_runtime_profile_for_execution_context( - scheduler_execution_context - ) - is SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE - ) + profile in NATIVE_GOAL_RUNTIME_PROFILES + and profile in GUIDED_START_TURN_RUNTIME_PROFILES ): begin_turn = True if heartbeat_turn_receipt and begin_turn: diff --git a/skills/loopx-project/references/heartbeat-hosts.md b/skills/loopx-project/references/heartbeat-hosts.md index 86e30ccca3..581f56e500 100644 --- a/skills/loopx-project/references/heartbeat-hosts.md +++ b/skills/loopx-project/references/heartbeat-hosts.md @@ -2,6 +2,37 @@ Read before creating, repairing or changing a recurring host loop. Save only the stable loader and read its fresh result on every wake; the current body owns execution, runtime turn identity and settlement. +## Codex App Native Goal + +When the user chooses native Goal continuation in an existing local Codex App +conversation, including Windows, load the bound Goal with this explicit profile: + +```bash +loopx --format json --registry heartbeat-prompt \ + --bootstrap --thin --runtime-profile codex_app_goal \ + --goal-id --agent-id +``` + +Require `ok=true` and use the returned `task_body` through the current App's +native Goal controls. Preserve the existing objective, identity and permissions; +do not clear or recreate an unfinished Goal merely to change its LoopX entry. +An already active Goal continues in the same conversation. A blocked Goal needs +the host's explicit user resume action; if that control is unavailable, report +the exact host limitation rather than starting another executor. + +`codex_app_goal` means `codex_app` / `goal_runtime` / `interactive` and uses +`visible-goal` settlement. Each iteration follows the fresh interaction contract, +including `--begin-turn` when requested, and reuses its returned identity for +writeback and settlement. Do not invent a native turn ID or persist a batch ID +in the loader. Do not create a heartbeat automation, apply RRULE changes, or +switch to a separate Codex CLI session for this profile. + +The existing `--codex-app` alias and `codex-app` onboarding remain heartbeat +defaults. They do not prove native Goal continuation. Verify the App's actual +Goal-triggered turn, admitted work, `visible-goal` settlement and subsequent +native continuation separately; a LoopX receipt alone does not establish the +host trigger. + ## Set Up Recurring Heartbeats When the exact host is Codex App connected to a remote workspace over SSH and diff --git a/tests/control_plane/test_context_machine_configuration.py b/tests/control_plane/test_context_machine_configuration.py index 1b6f6ce14a..2d8c9dc5a8 100644 --- a/tests/control_plane/test_context_machine_configuration.py +++ b/tests/control_plane/test_context_machine_configuration.py @@ -103,7 +103,7 @@ def test_machine_on_preserves_existing_worker_environment_off(tmp_path, monkeypa @pytest.mark.parametrize("host", [ ["--runtime-profile", profile] for profile in - ("trae_app", "claude_code", "kunluncode", "kiro_cli", "ark_managed_agent_goal") + ("trae_app", "claude_code", "kunluncode", "kiro_cli", "ark_managed_agent_goal", "codex_app_goal") ] + [["--host-surface", "claude_code", "--scheduler-owner", "agent_cli_loop", "--execution-mode", "interactive"]]) def test_codex_machine_preference_preserves_other_declared_hosts(tmp_path, monkeypatch, host): diff --git a/tests/control_plane/test_heartbeat_cli.py b/tests/control_plane/test_heartbeat_cli.py index b83d10cd34..163687f084 100644 --- a/tests/control_plane/test_heartbeat_cli.py +++ b/tests/control_plane/test_heartbeat_cli.py @@ -2,6 +2,7 @@ from __future__ import annotations import json +import shlex from pathlib import Path import pytest @@ -59,6 +60,27 @@ def test_heartbeat_output_and_format_precedence(tmp_path, global_format, local_f assert "--runtime-root" in payload["task_body"] +def test_codex_app_goal_bootstrap_loads_the_live_native_contract(tmp_path): + registry, runtime = write_command_fixture(tmp_path) + args = ["--format=json", *heartbeat_args(registry, runtime), + "--runtime-profile=codex_app_goal", "--thin", "--bootstrap"] + bootstrap = run_cli_main("loopx.entrypoint", args) + assert bootstrap.returncode == 0, bootstrap.stderr + payload = json.loads(bootstrap.stdout) + assert payload["ok"] is True + command = shlex.split(payload["task_body"].split("```sh\n", 1)[1].split("\n```", 1)[0]) + assert "--bootstrap" not in command + assert command[command.index("--runtime-profile") + 1] == "codex_app_goal" + + loaded = run_cli_main("loopx.entrypoint", command[1:]) + assert loaded.returncode == 0, loaded.stderr + prompt = json.loads(loaded.stdout) + assert prompt["ok"] is True + assert "--begin-turn" in prompt["quota_guard_command"] + assert "LOOPX_TURN=" not in prompt["task_body"] + assert "--source visible-goal" in prompt["quota_spend_command"] + + def test_heartbeat_semantic_errors_keep_recovery_and_exit_status(tmp_path): registry, runtime = write_command_fixture(tmp_path) base = ["--format=json", *heartbeat_args(registry, runtime)] diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 2b64bec8bd..dd17c7cefa 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -4093,8 +4093,10 @@ def test_guided_start_begins_one_turn_and_executes_returned_selection( assert _heartbeat_receipt_count(runtime, turn_instance_id) == 2 +@pytest.mark.parametrize("runtime_profile", ["codex_app_ssh_goal", "codex_app_goal"]) def test_visible_goal_continuation_begins_turn_and_executes_returned_selection( tmp_path: Path, + runtime_profile: str, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_selectable_alternative(project) @@ -4102,7 +4104,7 @@ def test_visible_goal_continuation_begins_turn_and_executes_returned_selection( prompt = build_heartbeat_prompt( goal_id=GOAL_ID, agent_id=AGENT_ID, - runtime_profile="codex_app_ssh_goal", + runtime_profile=runtime_profile, thin=True, ) guard_command = prompt["quota_guard_command"].replace( @@ -4325,20 +4327,29 @@ def test_unclaimed_candidate_does_not_create_an_unsettled_work_obligation( assert "action_portfolio" not in replay +@pytest.mark.parametrize("runtime_profile", ["codex_app_ssh_goal", "codex_app_goal"]) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_visible_goal_refresh_and_spend_preserve_selected_todo_causality( tmp_path: Path, + runtime_profile: str, + provider: str, + monkeypatch: pytest.MonkeyPatch, ) -> None: + if provider == "sqlite": + from canonical_authority_fixture import isolate_sqlite_runtime + + isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry_path = _write_fixture(tmp_path) _configure_read_only_todo(project) - _initialize_fixture_authority(project, runtime) + _initialize_fixture_authority(project, runtime, provider=provider) guard_rc, guard = _run_cli( registry_path, runtime, "quota", "should-run", "--runtime-profile", - "codex_app_ssh_goal", + runtime_profile, "--goal-id", GOAL_ID, "--agent-id", @@ -4398,6 +4409,10 @@ def test_visible_goal_refresh_and_spend_preserve_selected_todo_causality( assert spend["delivery_workspace_causality"]["requirement"] == "not_required" assert _spend_run_count(runtime) == 1 + replay_rc, replay = _run_cli(registry_path, runtime, *shlex.split(command)[1:]) + assert replay_rc == 0, replay + assert _spend_run_count(runtime) == 1 + @pytest.mark.parametrize( ("agent_type", "expected_source"), @@ -4707,7 +4722,7 @@ def test_visible_goal_unbound_spend_recovers_delivery_after_capability_replan( assert _spend_run_count(runtime) == 1 -@pytest.mark.parametrize("profile", ["codex_app_ssh_goal", "codex_cli", "ark_managed_agent_goal"]) +@pytest.mark.parametrize("profile", ["codex_app_ssh_goal", "codex_app_goal", "codex_cli", "ark_managed_agent_goal"]) def test_unbound_visible_goal_spend_returns_typed_mismatch_without_receipt( tmp_path: Path, profile: str, ) -> None: @@ -4729,7 +4744,7 @@ def test_unbound_visible_goal_spend_returns_typed_mismatch_without_receipt( assert guard_rc == 0, guard actions = guard["interaction_contract"]["cli_channel"]["next_cli_actions"] assert len(actions) == 1 - if profile == "codex_app_ssh_goal": + if profile in {"codex_app_ssh_goal", "codex_app_goal"}: assert actions[0].endswith("--begin-turn") else: assert "--turn-instance-id" in actions[0] @@ -4767,7 +4782,7 @@ def test_unbound_visible_goal_spend_returns_typed_mismatch_without_receipt( plan = bound["interaction_contract"]["cli_channel"]["settlement_plan"] assert plan["identity"]["todo_id"] == TODO_ID assert plan["identity"]["agent_id"] == AGENT_ID - if profile != "codex_app_ssh_goal": + if profile not in {"codex_app_ssh_goal", "codex_app_goal"}: assert plan["identity"]["turn_instance_id"] == TURN_ID assert [step["kind"] for step in plan["ordered_steps"]] == [ "validation", "durable_writeback", "quota_spend", "terminal_closeout", @@ -4799,7 +4814,7 @@ def test_begin_turn_rejects_a_non_receipt_runtime_profile(tmp_path: Path) -> Non # generic_cli and mint their own identity instead. assert guard["reason"] == ( "--begin-turn requires runtime-profile codex_app_heartbeat, " - "trae_app, or codex_app_ssh_goal; every other host starts its turn by " + "trae_app, codex_app_goal, or codex_app_ssh_goal; every other host starts its turn by " "passing its own --turn-instance-id" ) @@ -6877,8 +6892,10 @@ def test_todoless_blocked_replan_settles_read_only_external_evidence_without_wor assert replay.get("unsettled_host_turn_recovery") is None +@pytest.mark.parametrize("runtime_profile", ["codex_app_ssh_goal", "codex_app_goal"]) def test_unbound_visible_goal_todoless_replan_reenters_through_guided_turn( tmp_path: Path, + runtime_profile: str, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_autonomous_replan_fixture(project, runtime, registry_path) @@ -6890,7 +6907,7 @@ def test_unbound_visible_goal_todoless_replan_reenters_through_guided_turn( "quota", "should-run", "--runtime-profile", - "codex_app_ssh_goal", + runtime_profile, "--goal-id", GOAL_ID, "--agent-id", diff --git a/tests/control_plane/test_scheduler_execution_context.py b/tests/control_plane/test_scheduler_execution_context.py index 21063780c0..5e8ddbb620 100644 --- a/tests/control_plane/test_scheduler_execution_context.py +++ b/tests/control_plane/test_scheduler_execution_context.py @@ -41,6 +41,7 @@ ("codex_app_server", "outer_controller", "isolated_headless"), ("ark_managed_agent", "goal_runtime", "interactive"), ("codex_app", "host_automation", "hosted_automation"), + ("codex_app", "goal_runtime", "interactive"), ("trae_app", "host_automation", "hosted_automation"), ("codex_app_ssh", "agent_cli_loop", "interactive"), ("local_scheduler", "host_automation", "hosted_automation"), @@ -74,6 +75,11 @@ ("codex_app", "host_automation", "hosted_automation"), " --codex-app", ), + ( + SchedulerRuntimeProfile.CODEX_APP_NATIVE_GOAL, + ("codex_app", "goal_runtime", "interactive"), + " --runtime-profile codex_app_goal", + ), ( SchedulerRuntimeProfile.TRAE_APP, ("trae_app", "host_automation", "hosted_automation"), @@ -224,6 +230,7 @@ def test_scheduler_execution_context_decision_table( assert context_projection["codex_app_applicability"] == ( "applicable" if resolution.ok and host_surface is HostSurface.CODEX_APP + and scheduler_owner is SchedulerOwner.HOST_AUTOMATION else "not_applicable" if resolution.ok else "blocked_invalid_context" ) if values not in VALID_COMBINATIONS: @@ -1138,6 +1145,7 @@ def test_goal_runtime_terminal_stop_projects_complete() -> None: ("profile", "runtime_key"), ( (SchedulerRuntimeProfile.CODEX_APP_SSH_VISIBLE, "codex_app_ssh_goal"), + (SchedulerRuntimeProfile.CODEX_APP_NATIVE_GOAL, "codex_cli_tui"), (SchedulerRuntimeProfile.CODEX_CLI_VISIBLE, "codex_cli_tui"), ), ) diff --git a/tests/control_plane_ts/usage_statistics_goals.test.ts b/tests/control_plane_ts/usage_statistics_goals.test.ts index e32bf32bca..8b8de1101f 100644 --- a/tests/control_plane_ts/usage_statistics_goals.test.ts +++ b/tests/control_plane_ts/usage_statistics_goals.test.ts @@ -107,6 +107,12 @@ test("closed contract rejects identifiers, timestamps and extra fields; long dur assert.equal(goalDuration(30 * day), "gte_30d"); }); +test("Codex App native Goal and heartbeat share the desktop usage label", () => { + assert.equal(hostCategory("codex_app_goal"), "codex_app"); + assert.equal(hostCategory("codex_app_heartbeat"), "codex_app"); + assert.equal(hostCategory("codex_cli"), "codex_cli"); +}); + test("Kiro CLI has its own fixed Host label instead of other/unknown", () => { // Quota observations pass the runtime profile; activation and turns pass the // agent type. Both spellings must land on the same closed label. diff --git a/tests/test_host_loop_activation.py b/tests/test_host_loop_activation.py index 5ee6ddd239..447ee53e34 100644 --- a/tests/test_host_loop_activation.py +++ b/tests/test_host_loop_activation.py @@ -200,6 +200,7 @@ def test_first_class_hosts_bind_one_runtime_profile( ("runtime_profile", "expected"), ( ("ark_managed_agent_goal", True), + ("codex_app_goal", True), ("codex_app_ssh_goal", True), ("codex_cli", True), ("codex_app_heartbeat", False), @@ -328,7 +329,7 @@ def test_deepseek_harness_native_is_distinct_same_session_host() -> None: @pytest.mark.parametrize( "runtime_profile", - ("ark_managed_agent_goal", "codex_app_ssh_goal"), + ("ark_managed_agent_goal", "codex_app_ssh_goal", "codex_app_goal"), ) def test_goal_hosts_delegate_spend_to_live_settlement_not_static_templates( runtime_profile: str, @@ -393,7 +394,7 @@ def test_heartbeat_prompt_commands_keep_explicit_runtime_root() -> None: @pytest.mark.parametrize( "runtime_profile", - ("ark_managed_agent_goal", "codex_app_ssh_goal"), + ("ark_managed_agent_goal", "codex_app_ssh_goal", "codex_app_goal"), ) def test_goal_hosts_enter_live_contract_without_a_mandatory_skill_detour( runtime_profile: str, @@ -429,6 +430,7 @@ def test_goal_hosts_reuse_thin_dispatch_and_stay_compact() -> None: runtime_profile="codex_app_heartbeat", ) goal_hosts = [ + build_heartbeat_prompt(**common, runtime_profile="codex_app_goal"), build_heartbeat_prompt(**common, runtime_profile="codex_app_ssh_goal"), build_heartbeat_prompt(**common, runtime_profile="codex_cli"), build_heartbeat_prompt(**common, runtime_profile="ark_managed_agent_goal"), @@ -487,6 +489,7 @@ def test_native_codex_goal_wait_rule_matches_blocked_resume_contract() -> None: ("runtime_profile", "expected_host"), ( ("ark_managed_agent_goal", "Ark Managed Agent goal prompt"), + ("codex_app_goal", "visible Codex /goal task body"), ("codex_app_ssh_goal", "visible Codex /goal task body"), ("codex_cli", "visible Codex /goal task body"), ), diff --git a/tests/test_visible_goal_terminal_settlement.py b/tests/test_visible_goal_terminal_settlement.py index 6371742fd7..b3ce0c3da7 100644 --- a/tests/test_visible_goal_terminal_settlement.py +++ b/tests/test_visible_goal_terminal_settlement.py @@ -1,13 +1,16 @@ from __future__ import annotations +import pytest + from loopx.heartbeat_prompt import build_heartbeat_prompt -def test_visible_goal_delegates_settlement_then_checks_terminal_readback() -> None: +@pytest.mark.parametrize("runtime_profile", ["codex_app_ssh_goal", "codex_app_goal"]) +def test_visible_goal_delegates_settlement_then_checks_terminal_readback(runtime_profile: str) -> None: payload = build_heartbeat_prompt( goal_id="terminal-settlement-fixture", thin=True, - runtime_profile="codex_app_ssh_goal", + runtime_profile=runtime_profile, ) task_body = " ".join(payload["task_body"].split())