Exact model policy in start_model.go
apps/backend/internal/agent/runtime/lifecycle/start_model.go ↗The policy now requires an exact advertised match and never infers a bracketed variation.
Policy
func applyStartModelPolicy(
ctx context.Context,
log *logger.Logger,
applier modelApplier,
state *CachedModelState,
policy StartModelPolicy,
) (ModelSelectionDecision, error) {
if policy.Model == "" {
return ModelSelectionDecision{Outcome: ModelSelectionOutcomeNone}, nil
}
advertised := advertisedModelIDs(state)
if len(advertised) == 0 {
return unavailableStartModel(state, policy, ModelSelectionReasonCatalogEmpty)
}
if !containsModel(advertised, policy.Model) {
if !policy.AutoFallback && policy.FallbackModel != "" && containsModel(advertised, policy.FallbackModel) {
return applyAdvertisedFallback(ctx, log, applier, state, policy, decision)
}
reason := ModelSelectionReasonRequestedNotAdvertised
if !policy.AutoFallback && policy.FallbackModel != "" {
reason = ModelSelectionReasonFallbackNotAdvertised
}
return unavailableStartModel(state, policy, reason)
}
decision.SetModelCalled = true
if err := applier.SetModel(ctx, policy.Model); err != nil {
if sessionmodel.IsMethodNotFound(err) {
unsupported, unavailableErr := unavailableStartModel(state, policy, ModelSelectionReasonSelectionUnsupported)
unsupported.SetModelCalled = true
return unsupported, unavailableErr
}
if policy.AutoFallback {
decision = providerDefaultDecision(state, policy, ModelSelectionReasonSelectionFailedAutoFallback)
decision.SetModelCalled = true
return decision, nil
}
return decision, fmt.Errorf("failed to set start model %q: %w", policy.Model, err)
}
decision.EffectiveModel = policy.Model
decision.Outcome = ModelSelectionOutcomeApplied
return decision, nil
}
Outcomes
const (
ModelSelectionOutcomeNone ModelSelectionOutcome = ""
ModelSelectionOutcomeApplied ModelSelectionOutcome = "applied"
ModelSelectionOutcomeExplicitFallback ModelSelectionOutcome = "explicit_fallback"
ModelSelectionOutcomeProviderDefault ModelSelectionOutcome = "provider_default"
)
const (
ModelSelectionReasonRequestedNotAdvertised = "requested_not_advertised"
ModelSelectionReasonFallbackNotAdvertised = "fallback_not_advertised"
ModelSelectionReasonCatalogEmpty = "catalog_empty"
ModelSelectionReasonSelectionUnsupported = "selection_unsupported"
ModelSelectionReasonSelectionFailedAutoFallback = "selection_failed_auto_fallback"
)
Reset and rebind now fail closed
apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go ↗A fresh ACP session never continues on the provider default after losing an exact model. The reset path returns the policy error and marks the execution failed.
Diff
func (m *Manager) reapplySessionModelAfterReset(
ctx context.Context,
execution *AgentExecution,
newSessionID, modelID string,
) error {
client, releaseClient := execution.AcquireAgentCtlClient()
defer releaseClient()
return m.reapplySessionModelAfterResetWithClient(ctx, execution, client, newSessionID, modelID)
}
func (m *Manager) reapplySessionModelAfterResetWithClient(
ctx context.Context,
execution *AgentExecution,
client *agentctlclient.Client,
newSessionID, modelID string,
) error {
if client == nil || modelID == "" {
return nil
}
policy := m.resolveStartModelPolicy(ctx, execution.AgentProfileID)
policy.Model = modelID
decision, err := applyStartModelPolicy(
ctx, m.logger, client, execution.GetModelState(), policy,
)
if err != nil {
m.logger.Warn("failed to re-apply session model after context reset",
zap.String("execution_id", execution.ID),
zap.String("model", modelID),
zap.Error(err))
return err
}
if decision.Warning && m.sessionManager != nil {
m.sessionManager.publishModelSelectionWarningEvent(execution, newSessionID, decision)
}
if decision.EffectiveModel != "" &&
(decision.Outcome == ModelSelectionOutcomeApplied ||
decision.Outcome == ModelSelectionOutcomeExplicitFallback ||
decision.Outcome == ModelSelectionOutcomeUniqueVariation) {
decision.Outcome == ModelSelectionOutcomeExplicitFallback) {
m.logger.Info("re-applied session model after context reset",
zap.String("execution_id", execution.ID),
zap.String("session_id", execution.SessionID),
zap.String("new_acp_session_id", newSessionID),
zap.String("model", decision.EffectiveModel),
zap.Bool("using_fallback", decision.Outcome == ModelSelectionOutcomeExplicitFallback))
}
return nil
}
Tests now expect rejection
func TestReapplySessionModel_RejectsUnadvertisedExactModel(t *testing.T) {
mgr := newTestManager(t)
mock := newRestartMockAgentctlServer(t, false, false)
client := createTestClient(t, mock.server.URL)
exec := runtimeConfigResetExecution(client, true)
exec.SetModelState(&CachedModelState{
CurrentModelID: "provider-default",
Models: []streams.SessionModelInfo{{ModelID: "opus[1m]"}},
})
err := mgr.reapplySessionModelAfterReset(ctx, exec, "reset-session", "opus")
require.ErrorContains(t, err, "requested_not_advertised")
require.Empty(t, mock.getSetModelIDs())
}
Launch-time exact enforcement in session.go
apps/backend/internal/agent/runtime/lifecycle/session.go ↗The effective model is decided before any layer applies it. A strict unavailable model fails the launch before a prompt or tool call.
Effective model
func (sm *SessionManager) applyStartModelPolicyToEffectiveModel(
ctx context.Context,
execution *AgentExecution,
acpSessionID string,
profileModel, runtimeModel string,
startModelPolicy StartModelPolicy,
) (effective struct {
model string
appliedModel string
handled bool
decision ModelSelectionDecision
err error
}) {
effective.model = profileModel
if runtimeModel != "" {
effective.model = runtimeModel
}
client, releaseClient := execution.AcquireAgentCtlClient()
defer releaseClient()
if effective.model == "" || client == nil {
return effective
}
effective.handled = true
startModelPolicy.Model = effective.model
decision, policyErr := applyStartModelPolicy(
ctx, sm.logger, client,
execution.GetModelState(), startModelPolicy,
)
if policyErr != nil {
sm.logger.Error("start model unavailable, failing session start",
zap.String("execution_id", execution.ID),
zap.Error(policyErr))
effective.err = policyErr
return effective
}
effective.decision = decision
if decision.Outcome == ModelSelectionOutcomeApplied ||
decision.Outcome == ModelSelectionOutcomeExplicitFallback {
effective.appliedModel = decision.EffectiveModel
}
if decision.Outcome == ModelSelectionOutcomeExplicitFallback {
effective.model = decision.EffectiveModel
}
return effective
}
Caller neutralizes runtime override
effectiveModel := sm.applyStartModelPolicyToEffectiveModel(
ctx, execution, result.SessionID, profileModel, runtimeModel, startModelPolicy,
)
if effectiveModel.err != nil {
return effectiveModel.err
}
if effectiveModel.decision.Warning {
sm.publishModelSelectionWarningEvent(execution, result.SessionID, effectiveModel.decision)
}
profileModel = effectiveModel.model
runtimeModel = ""
execution.setSessionInitialized(true)
Frontend removes variation inference
apps/web/components/task-create-dialog-options.tsx ↗The host probe is now an editing hint only. A missing model shows a warning but never removes the profile or infers a variation.
Host probe warning
const advertised = advertisedModelIDs(availableAgents, profile.agent_name);
const startModelGone = Boolean(
profile.model && advertised.length > 0 && !advertised.includes(profile.model),
);
let modelProbeNote: string | undefined;
if (startModelGone) {
modelProbeNote = t("settings:profileStartModelNotAdvertisedOnHost", {
model: profile.model,
});
}
// The host-utility probe is an editing hint only. The selected
// executor owns the launch-time model catalog, so a host-only mismatch
// must never remove a profile from the task selector.
Deleted helper
// Deleted: apps/web/lib/model-variation.ts
// Deleted: apps/web/lib/model-variation.test.ts
// Previous helper inferred opus -> opus[1m] when one variation was advertised.
// Exact identity now requires the user to select the advertised ID explicitly.
Warning rendering and docs
apps/web/components/task/chat/messages/status-message.tsx ↗The UI keeps a compatibility path for the retired unique_variation reason and shows exact mismatch reasons with remediation.
Reason label
function modelSelectionReasonLabel(reason: string | undefined): string {
const keyByReason: Record<string, string> = {
requested_not_advertised: "task:modelSelectionReasonRequestedNotAdvertised",
fallback_not_advertised: "task:modelSelectionReasonFallbackNotAdvertised",
catalog_empty: "task:modelSelectionReasonCatalogEmpty",
selection_unsupported: "task:modelSelectionReasonUnsupported",
selection_failed_auto_fallback: "task:modelSelectionReasonAutoFallback",
};
if (reason === "unique_variation_applied") {
return t("task:modelSelectionReasonUniqueVariation");
}
return reason
? t(keyByReason[reason] ?? "task:modelSelectionReasonUnknown", { reason })
: t(UNKNOWN_TASK_KEY);
}
ADR
# ADR-2026-09-06-exact-profile-model-identity: Enforce Exact Profile Model Identity
**Status:** accepted
A profile with a non-empty model, auto_fallback = false, and no fallback_model is exact.
Its configured model must be advertised and applied before inference.
If that cannot be attested, the session fails before a prompt, tool call, or agent output.
Kandev never sends SetModel for an unadvertised model.