Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Scripts/Fixtures/test-suite-contract-ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2325,6 +2325,7 @@ root/RepoPromptTests.SettingsJSONOnlyPersistenceTests/testUserInitiatedRecoveryB
root/RepoPromptTests.GlobalSettingsCrossWindowPropagationTests/testOracleModelChangePropagatesAcrossWindows root Tests/RepoPromptTests/Prompt/GlobalSettingsCrossWindowPropagationTests.swift RepoPromptTests.GlobalSettingsCrossWindowPropagationTests testOracleModelChangePropagatesAcrossWindows Root unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.001000 unreviewed retain_pending_review 0 PR #418 oracle model cross-window propagation
root/RepoPromptTests.GlobalSettingsCrossWindowPropagationTests/testOraclePropagationDoesNotFeedbackLoop root Tests/RepoPromptTests/Prompt/GlobalSettingsCrossWindowPropagationTests.swift RepoPromptTests.GlobalSettingsCrossWindowPropagationTests testOraclePropagationDoesNotFeedbackLoop Root unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.001000 unreviewed retain_pending_review 0 PR #418 cross-window re-sync no feedback loop
root/RepoPromptTests.GlobalSettingsCrossWindowPropagationTests/testContextBuilderPickerExplicitCommitPersistsDisplayedRuntimeFallback root Tests/RepoPromptTests/Prompt/GlobalSettingsCrossWindowPropagationTests.swift RepoPromptTests.GlobalSettingsCrossWindowPropagationTests testContextBuilderPickerExplicitCommitPersistsDisplayedRuntimeFallback Settings settings.context_builder.explicit_fallback_commit runtime_fallback,explicit_commit,global_selection deterministic_persistence root_swiftpm routine 1 isolated_settings_store,isolated_user_defaults Explicitly committing the displayed Context Builder fallback persists the selected provider and model even when property observers do not fire. Re-selecting the displayed fallback could leave MCP and UI selections out of sync. 0.001000 user_defaults method_local_values+defer retain 0 PR #463 explicit Context Builder fallback commit
root/RepoPromptTests.GlobalSettingsCrossWindowPropagationTests/testGlobalContextBuilderSelectionUpdatesPopoverProjectionAndLaunchAuthority root Tests/RepoPromptTests/Prompt/GlobalSettingsCrossWindowPropagationTests.swift RepoPromptTests.GlobalSettingsCrossWindowPropagationTests testGlobalContextBuilderSelectionUpdatesPopoverProjectionAndLaunchAuthority Settings settings.context_builder.global_selection_authority global_selection,popover_projection,launch_authority,persistence deterministic_persistence root_swiftpm routine 1 isolated_settings_store,isolated_user_defaults Global Agent Models selection is projected identically to the Context Builder popover and launch resolver. Global persisted Codex selection could be displayed as Claude and route launches through the wrong provider. 0.131000 user_defaults method_local_values+defer retain 0 Issue #561 global Context Builder model authority regression
root/RepoPromptTests.SourceTargetTestConstructorGuardTests/testSourceTargetContainsNoProvenanceOrBindingTestConstructors root Tests/RepoPromptTests/CodeMap/SourceTargetTestConstructorGuardTests.swift RepoPromptTests.SourceTargetTestConstructorGuardTests testSourceTargetContainsNoProvenanceOrBindingTestConstructors CodeMap codemap.source_target.no_test_constructors source_scan,test_fixture_symbol,test_only_constructor,authority,completion,validated_snapshot repository_guard root_swiftpm routine 3 SourceTreeScanFixture Every Swift file under Sources/RepoPrompt is scanned and fails if provenance, authority, or completion test-constructor symbols are compiled into the app target. A DEBUG-only backdoor could re-enter production Sources and bypass service-issued provenance or binding factories. 1.267000 filesystem,security repository_checkout test_case retain 0 Final Slice 1A P2 source-target constructor guard
root/RepoPromptTests.StoreBackedWorkspaceSearchConcurrencyMatrixTests/testConcurrentSameStoreAndSeparateStoreMatrixPreservesOrderingIsolationAndCleanup root Tests/RepoPromptTests/WorkspaceContext/Search/StoreBackedWorkspaceSearchConcurrencyMatrixTests.swift RepoPromptTests.StoreBackedWorkspaceSearchConcurrencyMatrixTests testConcurrentSameStoreAndSeparateStoreMatrixPreservesOrderingIsolationAndCleanup WorkspaceContext unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.626000 unreviewed retain_pending_review 0 initial census source line 22
root/RepoPromptTests.StoreBackedWorkspaceSearchLaneTests/testBypassAccessDoesNotWaitForHeldBroadPermit root Tests/RepoPromptTests/WorkspaceContext/Search/StoreBackedWorkspaceSearchLaneTests.swift RepoPromptTests.StoreBackedWorkspaceSearchLaneTests testBypassAccessDoesNotWaitForHeldBroadPermit WorkspaceContext unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.007500 unreviewed retain_pending_review 0 initial census source line 169
Expand Down Expand Up @@ -2495,6 +2496,7 @@ root/RepoPromptTests.WebSearchToolCardTests/testWebActionClassifierDistinguishes
root/RepoPromptTests.WebSearchToolCardTests/testWebActionSubtitleFormatting root Tests/RepoPromptTests/AgentMode/ToolCards/WebSearchToolCardTests.swift RepoPromptTests.WebSearchToolCardTests testWebActionSubtitleFormatting AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.000000 unreviewed retain_pending_review 0 initial census source line 163
root/RepoPromptTests.WebSearchToolCardTests/testWebSearchNamesStayDistinctFromFileSearchAndRouteAsKnownResult root Tests/RepoPromptTests/AgentMode/ToolCards/WebSearchToolCardTests.swift RepoPromptTests.WebSearchToolCardTests testWebSearchNamesStayDistinctFromFileSearchAndRouteAsKnownResult AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.000000 unreviewed retain_pending_review 0 initial census source line 5
root/RepoPromptTests.WebSearchToolCardTests/testWebSearchPresentationCoversLiveAndSummaryOnlyPayloads root Tests/RepoPromptTests/AgentMode/ToolCards/WebSearchToolCardTests.swift RepoPromptTests.WebSearchToolCardTests testWebSearchPresentationCoversLiveAndSummaryOnlyPayloads AgentMode unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.000000 unreviewed retain_pending_review 0 initial census source line 281
root/RepoPromptTests.WindowCloseCoordinatorLifecycleTests/testPreValidationHydrationStopsExplicitlyStartedDynamicModelPolling root Tests/RepoPromptTests/App/WindowCloseCoordinatorDecisionTests.swift RepoPromptTests.WindowCloseCoordinatorLifecycleTests testPreValidationHydrationStopsExplicitlyStartedDynamicModelPolling ContextBuilder context_builder.dynamic_polling.prevalidation_stale_poller_reconciliation codex,opencode,cursor,polling,validation,lifecycle deterministic_lifecycle root_swiftpm routine 4 shared_global_settings,user_defaults,tracked_window Pre-validation hydration reconciles explicitly started Codex, OpenCode, and Cursor pollers without starting new polling. Stale dynamic pollers can survive pre-validation restoration and leak per-window subscriptions. 0.000000 global_settings,user_defaults window_lifecycle retain 0 Issue #561 stale dynamic polling lifecycle regression
root/RepoPromptTests.WindowCloseCoordinatorLifecycleTests/testAPISettingsCloseDuringInitialLoadDoesNotStartProviderValidation root Tests/RepoPromptTests/App/WindowCloseCoordinatorDecisionTests.swift RepoPromptTests.WindowCloseCoordinatorLifecycleTests testAPISettingsCloseDuringInitialLoadDoesNotStartProviderValidation App unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.001000 unreviewed retain_pending_review 0 initial census source line 194
root/RepoPromptTests.WindowCloseCoordinatorLifecycleTests/testSuspendedGitContextRefreshDoesNotRetainViewModel root Tests/RepoPromptTests/App/WindowCloseCoordinatorDecisionTests.swift RepoPromptTests.WindowCloseCoordinatorLifecycleTests testSuspendedGitContextRefreshDoesNotRetainViewModel App unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.000000 unreviewed retain_pending_review 0 initial census source line 60
root/RepoPromptTests.WindowCloseCoordinatorLifecycleTests/testUnregisterDisposesPerWindowCodexModelSubscribersAndStopsOwnedClient root Tests/RepoPromptTests/App/WindowCloseCoordinatorDecisionTests.swift RepoPromptTests.WindowCloseCoordinatorLifecycleTests testUnregisterDisposesPerWindowCodexModelSubscribersAndStopsOwnedClient App unreviewed unreviewed root_swiftpm routine 1 unreviewed unreviewed 0.207000 unreviewed retain_pending_review 0 initial census source line 117
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ final class ContextBuilderAgentViewModel: ObservableObject {
selectedModel = AgentModel.resolvedModel(forRaw: selectedModelRaw, agentKind: selectedAgent) ?? .defaultModel
isRestoringState = false
}
updateDynamicModelPolling()
updateDynamicModelPolling(allowUnvalidatedDynamicPolling: true)
Comment on lines 529 to +532

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: In selectedAgent.didSet, dynamic model polling is started but then immediately cancelled by a subsequent settings persistence flow that runs on the next run loop.
Severity: MEDIUM

Suggested Fix

Refactor the logic to prevent the immediate cancellation of polling. One option is to delay the persistAgentModelToEffectiveProfile() call or modify the applyEffectiveAgentModel function to not unconditionally stop polling. Alternatively, the state management could be adjusted so that the settings application does not override the intent to start polling.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
Sources/RepoPrompt/Features/ContextBuilder/ViewModels/ContextBuilderAgentViewModel.swift#L529-L532

Potential issue: In the `didSet` observer for `selectedAgent`, a call to
`updateDynamicModelPolling(allowUnvalidatedDynamicPolling: true)` is made to start
polling for dynamic models. However, this is immediately followed by
`persistAgentModelToEffectiveProfile()`, which triggers a settings change. This change
schedules `applyEffectiveAgentModel()` on the next main run loop, which in turn calls
`updateDynamicModelPolling` with `allowUnvalidatedDynamicPolling: false`. This second
call cancels the polling that was just initiated, rendering the initial
`allowUnvalidatedDynamicPolling: true` flag ineffective.

Did we get this right? 👍 / 👎 to inform future reviews.

persistAgentModelToEffectiveProfile()
if let session = activeSession {
persistSessionConfig(session)
Expand Down Expand Up @@ -1003,7 +1003,10 @@ final class ContextBuilderAgentViewModel: ObservableObject {
guard let self else { return }
await handleComposeTabsWillClose(tabIDs)
}
updateDynamicModelPolling(startCursorPolling: false)
updateDynamicModelPolling(
startCursorPolling: false,
allowUnvalidatedDynamicPolling: false
)
}

func prepareForWindowClose() {
Expand Down Expand Up @@ -1040,18 +1043,25 @@ final class ContextBuilderAgentViewModel: ObservableObject {
}

private func resolvedPersistedContextBuilderSelection(workspaceID: UUID? = nil) -> AgentModelCatalog.NormalizedAgentSelection? {
guard let apiSettingsViewModel = promptManager.apiSettingsViewModel,
apiSettingsViewModel.isContextBuilderProviderValidationComplete
else {
guard let apiSettingsViewModel = promptManager.apiSettingsViewModel else {
return nil
}
let profile = settingsManager.effectiveAgentModelsProfile(workspaceID: workspaceID ?? currentWorkspaceID)
let agentRaw = profile.contextBuilderAgentRaw
let modelRaw = agentRaw.flatMap { profile.contextBuilderModelsByAgent?[$0] }
let availability = apiSettingsViewModel.contextBuilderRestorationAvailabilityContext
guard apiSettingsViewModel.isContextBuilderProviderValidationComplete else {
return AgentModelCatalog.normalizePersistedSelection(
agentRaw: agentRaw,
modelRaw: modelRaw,
availability: availability,
codexDynamicModels: codexDynamicModels
)
}
return AutoRecommendationEngine.resolveContextBuilderSelection(
persistedAgentRaw: agentRaw,
persistedModelRaw: modelRaw,
availability: apiSettingsViewModel.contextBuilderRestorationAvailabilityContext,
availability: availability,
enabledRecommendationProviders: settingsManager.globalRecommendationProviderFilter()
)
}
Expand Down Expand Up @@ -1083,21 +1093,34 @@ final class ContextBuilderAgentViewModel: ObservableObject {
private func handleAgentProviderAvailabilityChanged() {
refreshAvailableAgents()
guard let normalized = resolvedPersistedContextBuilderSelection() else { return }
guard normalized.agent != selectedAgent || normalized.modelRaw.caseInsensitiveCompare(selectedModelRaw) != .orderedSame else {
return
let selectionChanged = normalized.agent != selectedAgent ||
normalized.modelRaw.caseInsensitiveCompare(selectedModelRaw) != .orderedSame
if selectionChanged {
isRestoringState = true
selectedAgent = normalized.agent
selectedModelRaw = normalized.modelRaw
selectedModel = AgentModel.resolvedModel(forRaw: normalized.modelRaw, agentKind: normalized.agent) ?? .defaultModel
isRestoringState = false
}
isRestoringState = true
selectedAgent = normalized.agent
selectedModelRaw = normalized.modelRaw
selectedModel = AgentModel.resolvedModel(forRaw: normalized.modelRaw, agentKind: normalized.agent) ?? .defaultModel
isRestoringState = false
updateDynamicModelPolling()
updateDynamicModelPolling(
startCursorPolling: false,
allowUnvalidatedDynamicPolling: false
)
Comment on lines +1105 to +1108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: After provider validation, handleAgentProviderAvailabilityChanged calls updateDynamicModelPolling with startCursorPolling: false, preventing the dynamic model subscription for Cursor from starting.
Severity: MEDIUM

Suggested Fix

In handleAgentProviderAvailabilityChanged, ensure the call to updateDynamicModelPolling passes startCursorPolling: true when the validation for the Cursor provider completes successfully. This will allow the guard condition in updateCursorModelPolling to pass and initiate the dynamic model subscription.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
Sources/RepoPrompt/Features/ContextBuilder/ViewModels/ContextBuilderAgentViewModel.swift#L1105-L1108

Potential issue: When agent provider validation completes, the
`handleAgentProviderAvailabilityChanged` function calls `updateDynamicModelPolling` with
the `startCursorPolling` parameter set to `false`. This causes a guard condition within
the downstream `updateCursorModelPolling` function to fail, preventing the
`startCursorModelsSubscriptionIfNeeded()` method from being called. As a result, for a
user with the Cursor agent selected, the list of dynamic models will not be updated upon
validation completion and will only refresh after a subsequent user action.

Did we get this right? 👍 / 👎 to inform future reviews.

}

private func updateDynamicModelPolling(startCursorPolling: Bool = true) {
updateCodexModelPolling()
updateOpenCodeModelPolling()
updateCursorModelPolling(startPolling: startCursorPolling)
private func updateDynamicModelPolling(
startCursorPolling: Bool = true,
allowUnvalidatedDynamicPolling: Bool = false
) {
if allowUnvalidatedDynamicPolling || promptManager.apiSettingsViewModel?.isContextBuilderProviderValidationComplete == true {
updateCodexModelPolling()
updateOpenCodeModelPolling()
updateCursorModelPolling(startPolling: startCursorPolling)
} else {
stopCodexModelsSubscription()
stopOpenCodeModelsSubscription()
stopCursorModelsSubscription()
}
}

private func updateCodexModelPolling() {
Expand Down Expand Up @@ -1138,6 +1161,26 @@ final class ContextBuilderAgentViewModel: ObservableObject {
codexModelsSubscriptionTask != nil
}

var test_hasCursorModelsSubscriptionTask: Bool {
cursorModelsSubscriptionTask != nil
}

var test_hasOpenCodeModelsSubscriptionTask: Bool {
openCodeModelsSubscriptionTask != nil
}

func test_applyEffectiveAgentModelForLifecycle() {
applyEffectiveAgentModel()
}

func test_handleAgentProviderAvailabilityChangedForLifecycle() {
handleAgentProviderAvailabilityChanged()
}

func test_updateDynamicModelPollingForLifecycle() {
updateDynamicModelPolling()
}

func test_cancelAndDrainCodexModelsSubscription() async {
let task = codexModelsSubscriptionTask
task?.cancel()
Expand Down Expand Up @@ -1352,7 +1395,10 @@ final class ContextBuilderAgentViewModel: ObservableObject {
}

isRestoringState = false
updateDynamicModelPolling(startCursorPolling: false)
updateDynamicModelPolling(
startCursorPolling: false,
allowUnvalidatedDynamicPolling: false
)
}

private func applySessionToBindings(_ session: TabSession) {
Expand Down Expand Up @@ -1381,7 +1427,10 @@ final class ContextBuilderAgentViewModel: ObservableObject {
contextBuilderInstructions = session.contextBuilderInstructions
selectedContextBuilderPromptIDs = session.selectedContextBuilderPromptIDs
isRestoringState = false
updateDynamicModelPolling(startCursorPolling: false)
updateDynamicModelPolling(
startCursorPolling: false,
allowUnvalidatedDynamicPolling: false
)
}

private func clearBindings() {
Expand Down Expand Up @@ -1421,7 +1470,10 @@ final class ContextBuilderAgentViewModel: ObservableObject {
questionTimeoutSeconds = ContextBuilderDefaults.questionTimeoutSeconds
planTokenBudget = ContextBuilderDefaults.planTokenBudget
isRestoringState = false
updateDynamicModelPolling(startCursorPolling: false)
updateDynamicModelPolling(
startCursorPolling: false,
allowUnvalidatedDynamicPolling: false
)
}

private func updateRuntimeBindings(from session: TabSession) {
Expand All @@ -1448,7 +1500,7 @@ final class ContextBuilderAgentViewModel: ObservableObject {
// Per-tab selected follow-up type
selectedFollowUpType = session.selectedFollowUpType
isRestoringState = false
updateDynamicModelPolling()
updateDynamicModelPolling(allowUnvalidatedDynamicPolling: false)
}

/// Lightweight binding update for streaming hot path - only updates agentLog and toolCallCount.
Expand Down Expand Up @@ -1585,7 +1637,10 @@ final class ContextBuilderAgentViewModel: ObservableObject {
selectedModel = AgentModel.resolvedModel(forRaw: normalized.modelRaw, agentKind: normalized.agent) ?? .defaultModel
isRestoringState = false
refreshAvailableAgents()
updateDynamicModelPolling(startCursorPolling: false)
updateDynamicModelPolling(
startCursorPolling: false,
allowUnvalidatedDynamicPolling: false
)
}

/// Load workspace-scoped discovery defaults (token budget, enhancement mode, clarifying questions, plan budget).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,18 +488,24 @@ class PromptViewModel: ObservableObject {
}

private func resolvedPersistedContextBuilderSelection() -> AgentModelCatalog.NormalizedAgentSelection? {
guard let apiSettingsViewModel,
apiSettingsViewModel.isContextBuilderProviderValidationComplete
else {
guard let apiSettingsViewModel else {
return nil
}
let profile = currentAgentModelsProfile()
let agentRaw = profile.contextBuilderAgentRaw
let modelRaw = agentRaw.flatMap { profile.contextBuilderModelsByAgent?[$0] }
let availability = apiSettingsViewModel.contextBuilderRestorationAvailabilityContext
guard apiSettingsViewModel.isContextBuilderProviderValidationComplete else {
return AgentModelCatalog.normalizePersistedSelection(
agentRaw: agentRaw,
modelRaw: modelRaw,
availability: availability
)
}
return AutoRecommendationEngine.resolveContextBuilderSelection(
persistedAgentRaw: agentRaw,
persistedModelRaw: modelRaw,
availability: apiSettingsViewModel.contextBuilderRestorationAvailabilityContext,
availability: availability,
enabledRecommendationProviders: settingsManager.globalRecommendationProviderFilter()
)
}
Expand Down
Loading
Loading