Skip to content

Apply, clamp or refuse launch controls on every route - #2064

Merged
ppXD merged 1 commit into
mainfrom
fix/dispose-every-launch-control
Nov 7, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/dispose-every-launch-control

Conversation

@ppXD

@ppXD ppXD commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • An Auto launch routed to plan-map or single-agent silently dropped the operator's allowed model pool, persona pool, decision critic, delivery spec and integrate opt-in (only SupervisorDefinitionBuilder read them), and plan-map dropped the acceptance floor the composer demanded. Every route-dependent control (allowedModelIds, allowedAgentDefinitionIds, acceptanceChecks, tier, decisionReviewMode, deliverySpec, integrateBranches, requirePlanConfirmation, plannerReviewMode) now gets a LaunchControlDisposition (Applied / Clamped / NotApplicable / Refused, with the reason) from one ILaunchControlResolver that TaskLaunchService and TaskRoutePreviewService share; both results carry ControlDispositions. Any Refused stops the launch before a session or run exists with task_launch_control_refused (422), listing each refused control. The preview also runs the launch's own pool checks (EnsureModelsInTeamAsync / EnsureAgentDefinitionsInTeamAsync), so a pooled row disabled or a persona deleted since the composer loaded is the same not-found on both.
  • Model pool, by credentialed-model row id (model names are unique only per credential). Plan-map bounds the planner's catalog (WorkflowPlanRequest.AllowedModelIds) and every branch agent.run; single-agent bakes it on its agent; the agent.run manifest declares allowedModelIds. At dispatch HarnessModelReconciler chooses through AllowedModelPool: the pinned row, the authored model on the pinned credential, that model on any pooled credential, the pinned credential's best pooled row, then the pool default. That order now holds at the engine level: under a non-empty pool AgentDefinitionResolver leaves an unusable pinned row on the task instead of failing the node at staging (an unbounded task still fails closed). AgentRunExecutor runs and persists the chosen row and names any move on the timeline by cause, with model and credential names. A pool with nothing runnable fails the run with agent_model_pool_unresolvable (not retried, logged at Warning without a stack trace). Every escalation stays inside the pool and stamps the row it picked beside the model (every later revise round and the cold-launch record carry that row), so the output review avoids the model that did the work, live and after a re-attach. Quick makes the same choice at launch, so the frozen config shows the row that runs. A persona the pool excludes is refused on single-agent and plan-map, named by its name.
  • Acceptance floor, by route and workspace. Each builder advertises whether its argv adapter needs a repository (TaskProjectionAcceptanceContract.RequiresRepository; defaults to true). The single agent grades the argv in its own workspace (the executor's local acceptance verifier), so a repository-free floor on the single-agent lane is Applied. The supervisor grades against the branch it publishes, so without a repository its floor is refused; at Delivery/Unattended the tier disposition refuses once, with the remedies the floor names (bind a repository, or launch at Quick effort; Prototype when no floor is carried), never "author a check". Plan-map refuses the floor; a malformed argv (blank executable, a null or NUL argument) is refused by name and counts as absent. On Auto, a well-formed floor keeps the route on a lane that grades it in this workspace: a repo-less floor lands on the single agent, never Deep or plan-map. The move is named in RoutePlan.DegradedReason, and the confirm card suggests the tier the route took. Grounding is resolved before routing in both launch and route snapshot, and Signals.Ambiguous asks for confirmation.
  • Where this departs from the design brief (the code wins):
    • Builders stay pure (no DB), so Quick's clamp is chosen by the resolver and the builder bakes it. The dispatch choice is made in the reconciler and applied by the executor, which owns the task write. A plan-map pin outside the pool is reported Clamped without rewriting the profile: each branch is held at dispatch, and the synthesis keeps its model.
    • The repository requirement is a per-builder advertisement rather than a switch on the lane name, so a new builder states it (and is treated as needing a repository until it does).
    • IsWellFormedFloor keeps its own copy of the argv rule; the shared AcceptanceArgv normaliser is not on main yet.
  • Accepted residuals:
    • The route preview resolves grounding without EnsureSessionCanContinueAsync, so a Continue preview can update the session summary and call the summarising model, as the launch always did.
    • Dispositions are returned on the launch response and kept in the route snapshot's ResultJson, not stored on the run.
    • The capability degrade runs after the floor reroute (latent): an Auto Deep route with a floor and a repository whose supervisor lane is unavailable degrades to plan-map, where the floor is refused instead of rerouted.
    • Review agents run on the launch's own ReviewerModelId, which the pool does not bound (unchanged here).
    • The TaskLaunchAuto benchmark arm now confirms an ambiguous classification, as it already did a low-confidence one.
  • Follow-ups:
    • Composer: it reads the preview's verdict, so it now lets a repo-less argv floor launch on the single-agent lane. But its Delivery gate (LaunchTaskModal.tsx ~:323, the stale missing.push("an acceptance check") gate) still asks a repo-less Deep launch for a check, where the backend's remedy is to bind a repository or launch at Quick effort.
    • Deep same-name tie: RealSupervisorActionExecutor.Spawn.cs:1131 resolves a spawn's model through ModelPoolSelector.ResolveDispatchAsync, which breaks same-name ties by row id (OrderBy(m => m.Id)) and overwrites a persona's pinned credential.
    • Deep spawns choose their model inside the pool but do not stamp AllowedModelIds on the spawned AgentTask, so their revise-loop escalation is bounded by credential only.
    • Grade plan-map's integrated tree against the operator floor.
    • Have the composer render the dispositions and mark the suggested tier among the confirm card's options.

Test plan

  • Unit, full suite: 13,241 passed, 1 skipped (the environment-gated cross-device case in LocalRwxAtomicCreateTests), 0 failed. Focused classes: LaunchControlResolverTests 30, AllowedModelPoolTests 10, LlmEffortClassifierTests 42, TaskRoutePreviewServiceTests 24, TaskLaunchServiceQualityTierTests 6, EffortRouterTests 19, EffortRouterDegradeTests 1, EffortRouterGenericityTests 4, EffortPolicyTests 24, DeploymentAutonomyCeilingTests 38, FailureTaxonomyTests 8, SingleAgentDefinitionBuilderTests 38, PlanMapSynthDefinitionBuilderTests 34, SupervisorDefinitionBuilderTests 30, AgentCodeNodeTests 151, PlanAuthorNodeTests 13, AgentRunExecutorEscalationTests 10, AgentRunExecutorOutputReviewTests 70, AgentDefinitionResolverTests 22

  • Integration (real Postgres):

    • Launch side: TaskLaunchFlowTests 59, TaskRoutePreviewFlowTests 8, TaskLaunchContractFlowTests 11, TaskRouteSnapshotFlowTests 11, EffortRouterFlowTests 3, PlannerModelPoolFlowTests 1, PlannerCassetteDriftTests 2 (ExpectedPlannerKey unchanged)
    • Dispatch side: HarnessModelReconcilerFlowTests 19, AgentDefinitionResolverCredentialedModelFlowTests 9, AgentDefinitionResolverFlowTests 22, AgentRunExecutorTests 152 (engine-staged pool and escalation-row tests included), AgentRunReviseLoopFlowTests 17, AgentNodeFlowTests 26, LocalAcceptanceExecutorFlowTests 14, AgentReviewerLoopFlowTests 2, CriticObservedModelFlowTests 20, AgentRunReattachFlowTests 15, SupervisorRichSpawnFlowTests 13
    • All green
  • E2E: TaskRouteSnapshotEndpointTests 20, TaskLaunchEndpointE2ETests 6, SpecPreviewArgvVocabularyTests 10, SupervisorWholeLoopE2ETests 14, PlanMapSynthFanoutFlowTests 2, PlanMapDynamicFanoutFlowTests 1, all green

  • 41 mutations. Each reverts one decision's key line, and the named test went red; every file was restored by copy and checked by hash.

    Mutation Test that went red
    single-agent advertises it needs a repository LaunchControlResolverTests.A_repo_less_argv_floor_on_the_single_agent_lane_is_applied_…; TaskLaunchFlowTests.A_repo_less_argv_floor_on_the_single_agent_lane_launches_and_its_agent_carries_the_check
    verdict ignores the repository LaunchControlResolverTests.A_repo_less_argv_floor_on_the_supervisor_lane_is_refused_…
    router grades as if a repository were bound LlmEffortClassifierTests.A_repo_less_operator_floor_moves_a_deep_route_to_the_single_agent_that_grades_it
    route request always says "repository" TaskLaunchFlowTests.An_auto_route_with_a_repo_less_floor_lands_on_the_single_agent_that_grades_it_never_on_deep
    tier tells a repo-less Deep launch to author a check LaunchControlResolverTests.A_repo_less_supervisor_launch_at_delivery_has_one_remedy_…; TaskLaunchFlowTests.A_launch_at_delivery_quality_without_an_acceptance_check_is_refused_as_its_preview_predicts (deep, no repo)
    engine fails the node for a pooled pin AgentRunExecutorTests.An_engine_staged_run_pinned_to_a_row_whose_credential_died_runs_another_pooled_row_and_says_so and …whose_pool_has_nothing_runnable_fails_typed_once_and_is_not_respawned
    escalation keeps the old row (task / persisted / live review) AgentRunExecutorTests.An_escalated_run_is_persisted_and_reviewed_as_the_row_that_did_the_work (three separate mutations)
    preview skips the model guard / the persona guard TaskRoutePreviewFlowTests.A_pool_member_gone_since_the_composer_loaded_… (the matching row each time)
    persona refused by id TaskLaunchFlowTests.An_auto_route_to_quick_refuses_a_persona_the_allowed_pool_excludes_before_any_run
    unresolvable pool logged at Error AgentRunExecutorTests.An_engine_staged_run_whose_pool_has_nothing_runnable_…
    manifest drops allowedModelIds AgentCodeNodeTests.The_manifest_declares_the_allowed_model_pool_the_node_reads
    escalation escapes the pool AgentRunReviseLoopFlowTests.A_run_bound_to_an_allowed_model_pool_never_escalates_outside_it
    chooser matches by name before the pinned row AllowedModelPoolTests.A_pinned_row_is_honoured_over_a_same_named_row_on_another_credential
    dispatch pins by model name only HarnessModelReconcilerFlowTests.The_same_model_on_another_pooled_credential_is_named_as_a_credential_move
    untyped pool failure / node retries it AgentRunExecutorTests.A_bounded_run_whose_pool_resolves_nothing_fails_rather_than_run_outside_it; AgentCodeNodeTests.A_resumed_failure_carries_the_retry_verdict_for_the_engine (that row)
    malformed floor counts as a floor LaunchControlResolverTests.A_malformed_floor_is_refused_by_name_…; LlmEffortClassifierTests.A_malformed_floor_is_no_routing_signal
    repo-less supervisor floor applied TaskLaunchFlowTests.A_repo_less_argv_floor_on_the_supervisor_lane_is_refused_with_the_verdict_its_preview_gives
    tier left out of the dispositions TaskLaunchFlowTests.A_launch_at_delivery_quality_without_an_acceptance_check_is_refused_as_its_preview_predicts
    preview / launch result drops dispositions; refusal not thrown TaskRoutePreviewServiceTests.Preview_reports_the_same_control_dispositions_…; TaskRoutePreviewFlowTests.The_preview_reports_the_same_control_dispositions_the_launch_then_applies; TaskLaunchFlowTests.An_explicit_standard_launch_with_an_operator_floor_is_refused_…
    Quick clamp skipped; planner sees the whole pool TaskLaunchFlowTests.An_auto_route_to_quick_clamps_a_pinned_model_…; PlannerModelPoolFlowTests.The_planners_catalog_lists_only_the_allowed_pool_…
    pool never baked / empty pool baked; node drops it SingleAgentDefinitionBuilderTests.The_allowed_model_pool_rides_…, PlanMapSynthDefinitionBuilderTests.The_allowed_model_pool_bounds_…; AgentCodeNodeTests.The_allowed_model_pool_a_projection_baked_is_carried_onto_the_task
    reconciler never consults the pool; executor ignores the pooled row HarnessModelReconcilerFlowTests.A_bounded_task_naming_a_model_outside_its_pool_runs_the_pools_default_row_and_says_so; AgentRunExecutorTests.A_bounded_run_naming_a_model_outside_its_pool_runs_the_pooled_row_…
    excluded persona applied; supervisor-only controls dropped TaskLaunchFlowTests.An_auto_route_to_quick_refuses_a_persona_…; LaunchControlResolverTests.Every_control_the_launch_carries_gets_exactly_one_disposition_per_lane
    plan-map adapter reports a floor compatible; auto floor not rerouted TaskLaunchContractFlowTests.A_standard_launch_carrying_an_operator_floor_is_refused_and_records_nothing; TaskLaunchFlowTests.An_auto_route_with_an_operator_floor_never_lands_on_plan_map_…
    launch or snapshot routes before grounding; ambiguity ignored TaskLaunchFlowTests.A_continue_is_routed_on_the_threads_grounding_by_both_the_preview_and_the_launch; LlmEffortClassifierTests.The_router_STILL_confirms_an_AMBIGUOUS_task_…
  • Pins changed deliberately:

    • TaskRoutePreviewServiceTests.Adapter_compatibility_comes_from_the_actual_builder_and_workspace: repo-less Quick is now Compatible, and RequiresRepository is true only on Deep.
    • The four mandate pins in TaskLaunchServiceQualityTierTests moved to LaunchControlResolverTests.The_tiers_floor_mandate_is_a_disposition_the_preview_can_predict.
    • In TaskLaunchFlowTests, the Deep ArgumentException pin and A_quick_launch_at_delivery_quality_is_never_rejected_for_a_missing_acceptance_check became one Theory: Deep with a repository and Quick without one are told to author a check; Deep without a repository is told to bind one.
    • The persona refusal is pinned by name instead of id.
    • TaskLaunchContractFlowTests.Every_launch_lane_records_original_controls_in_the_frozen_run_detail binds a repository only on the Deep row (Deep's capability key becomes GitBranch); its Standard row sends no floor, with that refusal pinned in its own test.
    • Fixtures that send Deep an argv floor bind a repository. The single-agent fixtures are back to main's repo-less form.
  • Added in the final round (each mutation of its line went red): later revise rounds and the cold-launch record keep the escalated row (AgentRunReviseLoopFlowTests.A_later_round_that_does_not_escalate_…, AgentRunExecutorTests.A_cold_launch_that_escalated_at_dispatch_…, the stored row in …A_round_that_over_claims_…); the persona tenancy filter (TaskLaunchFlowTests.A_persona_of_another_team_outside_the_pool_…); the dispatch team barrier (AgentDefinitionResolverCredentialedModelFlowTests Theory over revoked / disabled / foreign-team / missing, HarnessModelReconcilerFlowTests.A_pinned_row_of_another_team_…).

@ppXD
ppXD force-pushed the fix/dispose-every-launch-control branch from 0bcfcc1 to 39a5961 Compare October 11, 2026 11:37
@ppXD
ppXD force-pushed the fix/dispose-every-launch-control branch from 39a5961 to 20da248 Compare October 12, 2026 01:14
@ppXD
ppXD force-pushed the fix/dispose-every-launch-control branch 3 times, most recently from 4946ca7 to 4bb812d Compare November 7, 2026 17:08
An Auto launch that routed to plan-map or single-agent silently dropped
the operator's allowed model pool, persona pool, decision critic,
delivery spec and integrate opt-in (only the supervisor builder read
them), and plan-map dropped the acceptance floor while the composer
demanded one. The planner was shown the whole team pool, dispatch
never bounded the model it ran, and a failing check could escalate
past the pool.

Every route-dependent control now gets a disposition - Applied,
Clamped, NotApplicable or Refused, with the reason - from one resolver
the launch and its route preview share, so a preview can no longer
promise a launch it will not get. A refusal stops the launch before any
session or run exists, with task_launch_control_refused; the quality
tier's floor mandate is one of those dispositions, so the preview
predicts it too. The preview also runs the launch's own pool checks, so
a pool member gone since the composer loaded is the same not-found.

- The model pool bounds the plan-map planner's catalog and rides every
  agent.run. Dispatch runs a bounded task on the pooled row its pin
  names - by row, then model on credential, then model, then the
  credential's best row, then the pool default - names any move by
  cause on the run, fails with agent_model_pool_unresolvable (not
  retried) when nothing in the pool runs, and never escalates outside
  the pool. The engine leaves an unusable pinned row to that choice
  instead of failing the node at staging. Quick makes the same choice
  at launch, so the frozen config shows the row that runs.
- An escalation stamps the row it picked beside the model, and every
  later revise round and a cold-launch record carry that row, so the
  output review avoids the model that did the work, live and after a
  re-attach.
- A persona the pool excludes refuses a plan-map or quick launch, and
  is named, not shown as an id.
- The acceptance floor follows the route and workspace's own verdict.
  The single agent grades an argv in its own workspace, repository or
  not, so a repository-free floor there is applied; the supervisor
  grades against the branch it publishes, so without a repository its
  floor is refused, and a Delivery tier there is refused with the same
  remedies the floor names (bind a repository, or launch at Quick
  effort); with no floor carried it says to bind a repository or launch
  at Prototype. Plan-map refuses the
  floor, and a malformed one is refused by name. On Auto the floor
  keeps the route on a lane that grades it in this workspace, and the
  confirm card suggests the tier the route took.
- Grounding is resolved before routing, so a chat follow-up is
  classified as one, and an ambiguous classification asks to confirm.
@ppXD
ppXD merged commit 8ee5957 into main Nov 7, 2026
6 checks passed
@ppXD
ppXD deleted the fix/dispose-every-launch-control branch November 7, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant