fix(workflow): isolate exact model lane entry
This commit is contained in:
@@ -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,
|
||||
|
||||
+66
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user