fix(workflow): isolate exact model lane entry

This commit is contained in:
ayattara
2026-09-07 01:00:09 +00:00
parent 4080393183
commit bd899e84fc
4 changed files with 110 additions and 0 deletions
@@ -920,6 +920,8 @@ func (a *lifecycleAdapter) ResolveAgentProfile(ctx context.Context, profileID st
AgentName: info.AgentName,
Model: info.Model,
Mode: info.Mode,
FallbackModel: info.FallbackModel,
AutoFallback: info.AutoFallback,
ConfigOptions: info.ConfigOptions,
AutoApprove: info.AutoApprove,
DangerouslySkipPermissions: info.DangerouslySkipPermissions,
@@ -2781,6 +2781,14 @@ func (s *Service) prepareWorkflowStepSession(
effectiveProfile := s.resolveStepAgentProfile(ctx, step)
if effectiveProfile == "" || effectiveProfile == session.AgentProfileID {
if effectiveProfile != "" {
if s.workflowEntryRequiresFreshExactModelSession(ctx, session, step, sourceStep, effectiveProfile) {
endPolicy := s.resolveStepProfileSessionEndPolicy(sourceStep)
newSession, err := s.createNewSessionForStepWithEndPolicy(ctx, taskID, session, effectiveProfile, endPolicy)
if err != nil {
return nil, false, err
}
return newSession, true, nil
}
s.tagSessionAsWorkflowSwitched(ctx, session.ID)
}
if !session.IsPrimary {
@@ -2808,6 +2816,38 @@ func (s *Service) prepareWorkflowStepSession(
return newSession, true, nil
}
// workflowEntryRequiresFreshExactModelSession prevents a workflow lane from
// resuming a parked session whose persisted provider model disagrees with the
// same exact profile's configured model. Explicit session overrides remain
// valid within a lane; this boundary applies only while entering a different
// workflow step and only to profiles that did not authorize a fallback.
func (s *Service) workflowEntryRequiresFreshExactModelSession(
ctx context.Context,
session *models.TaskSession,
step, sourceStep *wfmodels.WorkflowStep,
profileID string,
) bool {
if session == nil || step == nil || sourceStep == nil || sourceStep.ID == step.ID || profileID == "" {
return false
}
profile, err := s.agentManager.ResolveAgentProfile(ctx, profileID)
if err != nil || profile == nil || profile.Model == "" || profile.AutoFallback || profile.FallbackModel != "" {
return false
}
effective, ok := models.LoadEffectiveSessionRuntimeConfig(session)
if !ok || effective.Model == "" || effective.Model == profile.Model {
return false
}
s.logger.Info("creating fresh workflow session for exact model identity",
zap.String("session_id", session.ID),
zap.String("profile_id", profileID),
zap.String("source_step_id", sourceStep.ID),
zap.String("target_step_id", step.ID),
zap.String("configured_model", profile.Model),
zap.String("persisted_model", effective.Model))
return true
}
func (s *Service) preflightWorkflowStepCredentials(
ctx context.Context,
taskID string,
@@ -651,6 +651,72 @@ func TestPrepareWorkflowStepSession_QAToPRDoesNotReuseSolRuntime(t *testing.T) {
require.Error(t, err, "the destination must not inherit the QA ACP resume identity before launch")
}
// TestPrepareWorkflowStepSession_HumanQAToWorkReplacesExactProfileRuntimeOverride
// covers a parked primary whose profile ID is already the Work profile but
// whose prior Human-QA turn persisted a different runtime model. Entering an
// exact Work lane must create a clean session before any auto-start prompt can
// resume that stale ACP identity.
func TestPrepareWorkflowStepSession_HumanQAToWorkReplacesExactProfileRuntimeOverride(t *testing.T) {
ctx := context.Background()
fixture := newProfileSwitchFixture(t, models.WorkflowProfileSessionStartPolicyReuse, models.WorkflowProfileSessionEndPolicyPark)
fixture.agentMgr.resolveProfileInfo = &executor.AgentProfileInfo{
Model: "gpt-5.6-terra",
}
fixture.current.AgentProfileID = "terra-work-profile"
fixture.current.AgentProfileSnapshot = map[string]interface{}{"model": "gpt-5.6-terra"}
fixture.current.Metadata = map[string]interface{}{
models.SessionMetaKeyRuntimeConfig: models.SessionRuntimeConfig{Model: "gpt-5.6-luna"},
}
require.NoError(t, fixture.repo.UpdateTaskSession(ctx, fixture.current))
workStep := &wfmodels.WorkflowStep{
ID: "step-work",
WorkflowID: "wf1",
AgentProfileID: "terra-work-profile",
ProfileSessionStartPolicy: models.WorkflowProfileSessionStartPolicyReuse,
}
humanQAStep := &wfmodels.WorkflowStep{
ID: "step-human-qa", WorkflowID: "wf1", AgentProfileID: "terra-work-profile",
ProfileSessionEndPolicy: models.WorkflowProfileSessionEndPolicyPark,
}
workSession, switched, err := fixture.svc.prepareWorkflowStepSession(ctx, "t1", fixture.current, workStep, humanQAStep)
require.NoError(t, err)
require.True(t, switched)
require.NotNil(t, workSession)
require.NotEqual(t, fixture.current.ID, workSession.ID)
require.Equal(t, "terra-work-profile", workSession.AgentProfileID)
_, inheritedRuntime := models.LoadSessionRuntimeConfig(workSession.Metadata)
require.False(t, inheritedRuntime, "a Work launch must not inherit Human-QA's Luna runtime model")
previous, err := fixture.repo.GetTaskSession(ctx, fixture.current.ID)
require.NoError(t, err)
require.False(t, previous.IsPrimary)
}
func TestPrepareWorkflowStepSession_HumanQAToWorkKeepsAuthorizedFallbackSession(t *testing.T) {
ctx := context.Background()
fixture := newProfileSwitchFixture(t, models.WorkflowProfileSessionStartPolicyReuse, models.WorkflowProfileSessionEndPolicyPark)
fixture.agentMgr.resolveProfileInfo = &executor.AgentProfileInfo{
Model: "gpt-5.6-terra", FallbackModel: "gpt-5.6-luna",
}
fixture.current.AgentProfileID = "terra-with-luna-fallback"
fixture.current.Metadata = map[string]interface{}{
models.SessionMetaKeyRuntimeConfig: models.SessionRuntimeConfig{Model: "gpt-5.6-luna"},
}
require.NoError(t, fixture.repo.UpdateTaskSession(ctx, fixture.current))
workStep := &wfmodels.WorkflowStep{
ID: "step-work", WorkflowID: "wf1", AgentProfileID: "terra-with-luna-fallback",
}
humanQAStep := &wfmodels.WorkflowStep{ID: "step-human-qa", WorkflowID: "wf1"}
workSession, switched, err := fixture.svc.prepareWorkflowStepSession(ctx, "t1", fixture.current, workStep, humanQAStep)
require.NoError(t, err)
require.False(t, switched)
require.Equal(t, fixture.current.ID, workSession.ID)
}
func TestSwitchSessionForStep_NewOnStartParkOnEndRoundTrip(t *testing.T) {
ctx := context.Background()
fixture := newProfileSwitchFixture(t, models.WorkflowProfileSessionStartPolicyNew, models.WorkflowProfileSessionEndPolicyPark)
@@ -380,6 +380,8 @@ type AgentProfileInfo struct {
AgentName string
Model string
Mode string
FallbackModel string
AutoFallback bool
ConfigOptions map[string]string
AutoApprove bool
DangerouslySkipPermissions bool