From c37654a7d981b476785db53397a85f09f43bbdfe Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Sat, 12 Sep 2026 02:45:27 +0000 Subject: [PATCH 1/4] chore(deps): update dependency system.drawing.common to 10.0.12 --- MeetingAssistant/MeetingAssistant.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MeetingAssistant/MeetingAssistant.csproj b/MeetingAssistant/MeetingAssistant.csproj index da76ee0..df11f7f 100644 --- a/MeetingAssistant/MeetingAssistant.csproj +++ b/MeetingAssistant/MeetingAssistant.csproj @@ -29,7 +29,7 @@ - + From bacef060fea156fa16f65b5068d2091f1eb5edb2 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Mon, 14 Sep 2026 02:41:00 +0000 Subject: [PATCH 2/4] chore(deps): update dependency ncalcsync to 7.2.0 --- MeetingAssistant/MeetingAssistant.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MeetingAssistant/MeetingAssistant.csproj b/MeetingAssistant/MeetingAssistant.csproj index da76ee0..e6c002d 100644 --- a/MeetingAssistant/MeetingAssistant.csproj +++ b/MeetingAssistant/MeetingAssistant.csproj @@ -26,7 +26,7 @@ - + From 93eb3d4e986407b0f3b523d8f59af212043064ce Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Wed, 16 Sep 2026 15:00:41 +0200 Subject: [PATCH 3/4] fix: prevent profile-switch transcription deadlock --- .../RecordingCoordinatorTests.cs | 40 +++++++++++++++---- .../Recording/MeetingRecordingCoordinator.cs | 25 ++++++++---- .../design.md | 2 + .../specs/meeting-recording/spec.md | 11 +++++ .../add-transcription-pause-controls/tasks.md | 11 +++++ 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/MeetingAssistant.Tests/RecordingCoordinatorTests.cs b/MeetingAssistant.Tests/RecordingCoordinatorTests.cs index 3bd994a..5720250 100644 --- a/MeetingAssistant.Tests/RecordingCoordinatorTests.cs +++ b/MeetingAssistant.Tests/RecordingCoordinatorTests.cs @@ -2386,12 +2386,17 @@ public sealed class RecordingCoordinatorTests await coordinator.StopAsync(CancellationToken.None); } - [Fact] - public async Task ToggleToDifferentLaunchProfileBuffersAudioCapturedWhilePreviousPipelineDrains() + [Theory] + [InlineData("default", "english")] + [InlineData("english", "default")] + public async Task ProfileSwitchDrainsFinalTranscriptAndResumesBufferedAudioWithoutBlockingNotifications( + string initialProfile, + string targetProfile) { var audioSource = new ControlledAudioSource(); var pipelineFactory = new BlockingProfileSwitchSpeechRecognitionPipelineFactory(); var transcriptStore = new InMemoryTranscriptStore(); + var promptService = new IgnoringMeetingInactivityPromptService(); var launchProfiles = CreateLaunchProfiles( Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "default"), Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "english")); @@ -2406,13 +2411,16 @@ public sealed class RecordingCoordinatorTests new CapturingMeetingSummaryPipeline(), Options.Create(launchProfiles.GetRequiredProfile(null).Options), NullLogger.Instance, + inactivityPromptService: promptService, launchProfiles: launchProfiles); - await coordinator.StartAsync(CancellationToken.None); + await coordinator.StartAsync(initialProfile, CancellationToken.None); await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None); - await transcriptStore.WaitForTextAsync("default chunk:2"); + await transcriptStore.WaitForTextAsync($"{initialProfile} chunk:2"); + await WaitUntilAsync(() => promptService.DismissAllCount == 1); - var switchTask = coordinator.ToggleAsync("english", CancellationToken.None); + using var switchCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var switchTask = coordinator.ToggleAsync(targetProfile, switchCancellation.Token); await pipelineFactory.WaitUntilFirstPipelineDrainIsBlockedAsync(); await audioSource.WriteAsync(new AudioChunk([1, 0, 2, 0, 3, 0], 16000, 1), CancellationToken.None); @@ -2420,10 +2428,21 @@ public sealed class RecordingCoordinatorTests pipelineFactory.ReleaseFirstPipelineDrain(); await switchTask.WaitAsync(TimeSpan.FromSeconds(5)); - await transcriptStore.WaitForTextAsync("english chunk:6"); + await transcriptStore.WaitForTextAsync($"{targetProfile} chunk:6"); + await WaitUntilAsync(() => promptService.DismissAllCount == 3); - Assert.Equal([null, "english"], pipelineFactory.ProfileNames); - await coordinator.StopAsync(CancellationToken.None); + Assert.Equal(targetProfile, coordinator.CurrentStatus.LaunchProfile); + Assert.Equal([initialProfile, targetProfile], pipelineFactory.ProfileNames); + var segments = transcriptStore.Segments.ToList(); + var finalResultIndex = segments.FindIndex(segment => segment.Text == "Final result emitted while switching profiles."); + var markerIndex = segments.FindIndex(segment => segment.Text.Contains($"Transcription profile changed to {targetProfile}", StringComparison.Ordinal)); + Assert.True(finalResultIndex >= 0 && finalResultIndex < markerIndex); + + await coordinator.ToggleAsync(initialProfile, CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5)); + await audioSource.WriteAsync(new AudioChunk([1, 0, 2, 0], 16000, 1), CancellationToken.None); + await transcriptStore.WaitForTextAsync($"{initialProfile} chunk:4"); + await coordinator.StopAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5)); + Assert.False(coordinator.CurrentStatus.IsRecording); } [Fact] @@ -5140,6 +5159,11 @@ public sealed class RecordingCoordinatorTests drainBlocked.TrySetResult(); await releaseDrain.Task.WaitAsync(cancellationToken); + yield return new TranscriptionSegment( + TimeSpan.Zero, + TimeSpan.FromSeconds(3), + "Unknown", + "Final result emitted while switching profiles."); } } diff --git a/MeetingAssistant/Recording/MeetingRecordingCoordinator.cs b/MeetingAssistant/Recording/MeetingRecordingCoordinator.cs index c15c180..03c57ed 100644 --- a/MeetingAssistant/Recording/MeetingRecordingCoordinator.cs +++ b/MeetingAssistant/Recording/MeetingRecordingCoordinator.cs @@ -805,7 +805,9 @@ public sealed class MeetingRecordingCoordinator run.RecordTranscriptActivity(segment, inactivityClock.Now); if (!string.IsNullOrWhiteSpace(segment.Text)) { - await DismissInactivityPromptsForRunAsync(run, cancellationToken); + // Switching profiles holds the coordinator gate while draining this reader. + // Notification cleanup must not make transcript draining wait for that gate. + _ = DismissInactivityPromptsForRunAsync(run, run.CaptureCancellation); } } } @@ -814,19 +816,26 @@ public sealed class MeetingRecordingCoordinator RecordingRun run, CancellationToken cancellationToken) { - await gate.WaitAsync(cancellationToken); try { - if (!ReferenceEquals(currentRun, run) || run.IsCaptureStopping) + await gate.WaitAsync(cancellationToken); + try { - return; - } + if (!ReferenceEquals(currentRun, run) || run.IsCaptureStopping) + { + return; + } - await DismissInactivityPromptsAsync(cancellationToken); + await DismissInactivityPromptsAsync(cancellationToken); + } + finally + { + gate.Release(); + } } - finally + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { - gate.Release(); + // Pending cleanup is no longer needed after this run stops capturing. } } diff --git a/openspec/changes/add-transcription-pause-controls/design.md b/openspec/changes/add-transcription-pause-controls/design.md index 65079e6..f48c356 100644 --- a/openspec/changes/add-transcription-pause-controls/design.md +++ b/openspec/changes/add-transcription-pause-controls/design.md @@ -46,6 +46,8 @@ The inactivity safeguard will skip transcript-inactivity prompting and its ordin The coordinator will call dismissal only after a non-empty live segment has been durably appended, matching the user's observable meaning of a new transcription being written. Notification callbacks will verify that their originating run is still current before changing recording state. +Transcript processing requests notification dismissal without awaiting it. Profile switching holds the coordinator gate while draining the old transcript reader, so awaiting dismissal from that reader would create a circular wait on the same gate. Deferred cleanup retains the gate and current-run check, uses the capture cancellation token, and handles cancellation when capture stops. Durable transcript activity still invalidates stale notification actions immediately, before deferred cleanup runs. + ### Model pause as one toggling tray action The active-recording tray menu will show `Pause transcription` while running and `Unpause transcription` while paused. It will stay in the fine-grained controls section below the dedicated `Finish meeting` section. The inactivity toast will add `Pause transcription` alongside the existing Yes/No stop controls. diff --git a/openspec/changes/add-transcription-pause-controls/specs/meeting-recording/spec.md b/openspec/changes/add-transcription-pause-controls/specs/meeting-recording/spec.md index 07b73f6..714f069 100644 --- a/openspec/changes/add-transcription-pause-controls/specs/meeting-recording/spec.md +++ b/openspec/changes/add-transcription-pause-controls/specs/meeting-recording/spec.md @@ -121,6 +121,8 @@ If the user selects pause from the stop prompt, Meeting Assistant SHALL pause tr When a new non-empty live transcript segment is written, Meeting Assistant SHALL dismiss all outstanding transcript-inactivity notifications and invalidate their pending actions. +Notification dismissal SHALL NOT block processing the final transcript segments emitted while switching launch profiles. Any deferred dismissal SHALL recheck that its originating run is still the active recording before dismissing notifications. + While transcription is intentionally paused, Meeting Assistant SHALL NOT show transcript-inactivity prompts and SHALL NOT apply the ordinary transcript-inactivity auto-stop threshold. Meeting Assistant SHALL normally stop a meeting that remains continuously paused for the configured maximum pause duration, defaulting to 4 hours, without first showing a transcript-inactivity notification. This maximum continuous-pause cutoff SHALL remain active when ordinary transcript-inactivity prompting and auto-stop are disabled. @@ -205,6 +207,15 @@ When a recording stops normally and its meeting note, transcript, and assistant - **WHEN** the older meeting writes a late transcript segment - **THEN** the newer meeting's notification and pending actions remain active +#### Scenario: Final transcript during a profile switch dismisses notifications without blocking the switch +- **GIVEN** a meeting is actively recording with either the `default` or `english` profile +- **WHEN** the user switches to the other profile +- **AND** the old recognizer emits a final non-empty transcript segment while draining +- **THEN** Meeting Assistant writes that final segment before the profile-switch marker +- **AND** completes the switch and transcribes buffered audio in the same meeting +- **AND** dismisses the originating active run's outstanding inactivity notifications +- **AND** subsequent profile switches and normal meeting completion remain available + #### Scenario: Empty stopped recording is cleaned up - **GIVEN** a recording is active - **AND** the meeting note, transcript, and assistant context only contain generated default content diff --git a/openspec/changes/add-transcription-pause-controls/tasks.md b/openspec/changes/add-transcription-pause-controls/tasks.md index c76000d..81c9d73 100644 --- a/openspec/changes/add-transcription-pause-controls/tasks.md +++ b/openspec/changes/add-transcription-pause-controls/tasks.md @@ -36,3 +36,14 @@ - [x] 6.2 Scope notification dismissal to the active run and move activity invalidation after durable transcript append, with overlapping-run and blocked-write regression tests. - [x] 6.3 Make pause-notification activity validation atomic and make tray pause/unpause actions intent-specific, with race and stale-menu tests. - [x] 6.4 Complete an independent simplification review and rerun all required validation before commit. + +## 7. Profile-switch deadlock repair + +- [x] 7.1 Reproduce final transcript delivery during profile switching in both directions through the coordinator's public interface. +- [x] 7.2 Decouple notification dismissal from transcript draining while preserving current-run checks and cancellation handling. +- [x] 7.3 Verify buffered transcription, subsequent controls, notification lifecycle tests, the full suite, Windows build, and strict OpenSpec validation. +- [x] 7.4 Deploy with explicit restart authorization, verify the live health/control endpoints, and record the operational verification limits. + +Verification on 2026-09-16: both profile-switch regression cases failed with the original circular wait and passed after the repair. All 520 solution tests passed on the final run. An unchanged audio-mixing timing test failed during the first full run, then passed alone and in the final full run. Strict OpenSpec validation and the Windows Release publish passed. The executable is staged at `tmp/meeting-assistant-runtime/run-20260916-142827-profile-switch-fix`. + +Operational verification on 2026-09-16: after explicit user authorization, the deadlocked process was killed and the staged Windows release started as PID 48716. `/health` returned `ok`, `/recording/status` returned idle, and the process path confirmed the fixed release. The old meeting's summary was requested through `/meetings/summary/retry`. Profile-switch behavior was verified through the public coordinator regression tests rather than recording a new live meeting. The existing 25,092,144-byte WAV and meeting artifacts were backed up under `tmp/profile-switch-recovery-20260916`; audio held only in the killed process after the switch was not recovered. The meeting context records that transcription gap. From ef0c0faba8cafcb18fb155a8b14d5f3597d53148 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Thu, 17 Sep 2026 02:45:15 +0000 Subject: [PATCH 4/4] chore(deps): update dependency microsoft.net.test.sdk to 18.10.1 --- MeetingAssistant.Tests/MeetingAssistant.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MeetingAssistant.Tests/MeetingAssistant.Tests.csproj b/MeetingAssistant.Tests/MeetingAssistant.Tests.csproj index 2d9086e..8cc0182 100644 --- a/MeetingAssistant.Tests/MeetingAssistant.Tests.csproj +++ b/MeetingAssistant.Tests/MeetingAssistant.Tests.csproj @@ -10,7 +10,7 @@ - +