15 Commits
Author SHA1 Message Date
codex fbd5a0852f Merge remote-tracking branch 'origin/renovate/naudio-2.x' into renovate/naudio-3.x
PR and Push Build/Test / build-and-test (push) Successful in 14m14s
PR and Push Build/Test / build-and-test (pull_request) Successful in 12m36s
# Conflicts:
#	MeetingAssistant/MeetingAssistant.csproj
2026-09-17 13:34:55 +02:00
codex 568bedb77a fix: isolate Windows sample playback for NAudio 3 2026-09-17 13:34:47 +02:00
codex f21c56b1c7 Merge remote-tracking branch 'origin/main' into renovate/naudio-3.x
# Conflicts:
#	MeetingAssistant/MeetingAssistant.csproj
2026-09-17 13:30:47 +02:00
codex 40d10d634a fix: reference WinMM explicitly for shared sample playback
PR and Push Build/Test / build-and-test (push) Successful in 11m36s
PR and Push Build/Test / build-and-test (pull_request) Successful in 12m9s
2026-09-17 13:27:49 +02:00
codex 264a3a823f Merge remote-tracking branch 'origin/main' into renovate/naudio-2.x
# Conflicts:
#	MeetingAssistant/MeetingAssistant.csproj
2026-09-17 13:26:04 +02:00
codex d7db64bec7 Merge pull request 'chore(deps): update dependency microsoft.net.test.sdk to 18.10.1' (#56) from renovate/microsoft.net.test.sdk-18.x into main
PR and Push Build/Test / build-and-test (push) Successful in 12m7s
2026-09-17 10:50:52 +02:00
codex cdeef1967e Merge pull request 'chore(deps): update dependency ncalcsync to 7.2.0' (#55) from renovate/ncalcsync-7.x into main
PR and Push Build/Test / build-and-test (push) Successful in 11m54s
2026-09-17 10:49:02 +02:00
codex aeb0cfe42f Merge pull request 'chore(deps): update dependency system.drawing.common to 10.0.12' (#52) from renovate/system.drawing.common-10.x into main
PR and Push Build/Test / build-and-test (push) Successful in 11m43s
2026-09-17 10:46:41 +02:00
renovate-bot ef0c0faba8 chore(deps): update dependency microsoft.net.test.sdk to 18.10.1
PR and Push Build/Test / build-and-test (push) Successful in 11m52s
PR and Push Build/Test / build-and-test (pull_request) Successful in 11m25s
2026-09-17 02:45:15 +00:00
codex 93eb3d4e98 fix: prevent profile-switch transcription deadlock
PR and Push Build/Test / build-and-test (push) Successful in 15m17s
2026-09-16 15:00:49 +02:00
Manuel b6acba8402 Merge branch 'main' into renovate/system.drawing.common-10.x
PR and Push Build/Test / build-and-test (push) Successful in 12m3s
PR and Push Build/Test / build-and-test (pull_request) Successful in 11m36s
2026-09-15 09:50:31 +02:00
Manuel 336d940acd Merge branch 'main' into renovate/naudio-2.x
PR and Push Build/Test / build-and-test (push) Failing after 7m28s
PR and Push Build/Test / build-and-test (pull_request) Failing after 7m17s
2026-09-15 09:50:03 +02:00
renovate-bot bacef060fe chore(deps): update dependency ncalcsync to 7.2.0
PR and Push Build/Test / build-and-test (pull_request) Successful in 10m47s
PR and Push Build/Test / build-and-test (push) Successful in 14m26s
2026-09-14 02:41:00 +00:00
renovate-bot c6a50d307e chore(deps): update dependency naudio to 2.4.0
PR and Push Build/Test / build-and-test (push) Failing after 1m48s
PR and Push Build/Test / build-and-test (pull_request) Failing after 1m48s
2026-09-12 02:45:52 +00:00
renovate-bot c37654a7d9 chore(deps): update dependency system.drawing.common to 10.0.12
PR and Push Build/Test / build-and-test (pull_request) Failing after 1m48s
PR and Push Build/Test / build-and-test (push) Failing after 1m47s
2026-09-12 02:45:27 +00:00
12 changed files with 113 additions and 19 deletions
@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using MeetingAssistant.Recording; using MeetingAssistant.Recording;
using MeetingAssistant.Workflow;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
@@ -41,6 +42,17 @@ public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Pr
Assert.Equal("ok", body?.Status); Assert.Equal("ok", body?.Status);
} }
[Fact]
public async Task NeutralBuildReportsSpeakerPlaybackUnavailable()
{
var playback = factory.Services.GetRequiredService<IWorkflowRulesEditorSamplePlaybackQueue>();
var result = await playback.QueueAsync(1, [1, 2]);
Assert.Contains("Windows build", result);
Assert.DoesNotContain("Queued", result);
}
[Fact] [Fact]
public async Task ApplicationStartupDeletesStaleTemporaryRecordings() public async Task ApplicationStartupDeletesStaleTemporaryRecordings()
{ {
@@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1" /> <PackageReference Include="coverlet.collector" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.12" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.12" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup> </ItemGroup>
@@ -2386,12 +2386,17 @@ public sealed class RecordingCoordinatorTests
await coordinator.StopAsync(CancellationToken.None); await coordinator.StopAsync(CancellationToken.None);
} }
[Fact] [Theory]
public async Task ToggleToDifferentLaunchProfileBuffersAudioCapturedWhilePreviousPipelineDrains() [InlineData("default", "english")]
[InlineData("english", "default")]
public async Task ProfileSwitchDrainsFinalTranscriptAndResumesBufferedAudioWithoutBlockingNotifications(
string initialProfile,
string targetProfile)
{ {
var audioSource = new ControlledAudioSource(); var audioSource = new ControlledAudioSource();
var pipelineFactory = new BlockingProfileSwitchSpeechRecognitionPipelineFactory(); var pipelineFactory = new BlockingProfileSwitchSpeechRecognitionPipelineFactory();
var transcriptStore = new InMemoryTranscriptStore(); var transcriptStore = new InMemoryTranscriptStore();
var promptService = new IgnoringMeetingInactivityPromptService();
var launchProfiles = CreateLaunchProfiles( 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"), "default"),
Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "english")); Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "english"));
@@ -2406,13 +2411,16 @@ public sealed class RecordingCoordinatorTests
new CapturingMeetingSummaryPipeline(), new CapturingMeetingSummaryPipeline(),
Options.Create(launchProfiles.GetRequiredProfile(null).Options), Options.Create(launchProfiles.GetRequiredProfile(null).Options),
NullLogger<MeetingRecordingCoordinator>.Instance, NullLogger<MeetingRecordingCoordinator>.Instance,
inactivityPromptService: promptService,
launchProfiles: launchProfiles); 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 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 pipelineFactory.WaitUntilFirstPipelineDrainIsBlockedAsync();
await audioSource.WriteAsync(new AudioChunk([1, 0, 2, 0, 3, 0], 16000, 1), CancellationToken.None); 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(); pipelineFactory.ReleaseFirstPipelineDrain();
await switchTask.WaitAsync(TimeSpan.FromSeconds(5)); 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); Assert.Equal(targetProfile, coordinator.CurrentStatus.LaunchProfile);
await coordinator.StopAsync(CancellationToken.None); 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] [Fact]
@@ -5140,6 +5159,11 @@ public sealed class RecordingCoordinatorTests
drainBlocked.TrySetResult(); drainBlocked.TrySetResult();
await releaseDrain.Task.WaitAsync(cancellationToken); await releaseDrain.Task.WaitAsync(cancellationToken);
yield return new TranscriptionSegment(
TimeSpan.Zero,
TimeSpan.FromSeconds(3),
"Unknown",
"Final result emitted while switching profiles.");
} }
} }
+2 -2
View File
@@ -26,10 +26,10 @@
<PackageReference Include="Microsoft.CognitiveServices.Speech.Extension.MAS" Version="$(MicrosoftSpeechVersion)" ExcludeAssets="build" /> <PackageReference Include="Microsoft.CognitiveServices.Speech.Extension.MAS" Version="$(MicrosoftSpeechVersion)" ExcludeAssets="build" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
<PackageReference Include="NAudio" Version="3.1.0" /> <PackageReference Include="NAudio" Version="3.1.0" />
<PackageReference Include="NCalcSync" Version="7.1.0" /> <PackageReference Include="NCalcSync" Version="7.2.0" />
<PackageReference Include="RazorLight" Version="2.3.1" /> <PackageReference Include="RazorLight" Version="2.3.1" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
<PackageReference Include="System.Drawing.Common" Version="10.0.11" /> <PackageReference Include="System.Drawing.Common" Version="10.0.12" />
<PackageReference Include="Whisper.net" Version="1.9.1" /> <PackageReference Include="Whisper.net" Version="1.9.1" />
<PackageReference Include="Whisper.net.Runtime" Version="1.9.1" /> <PackageReference Include="Whisper.net.Runtime" Version="1.9.1" />
<PackageReference Include="YamlDotNet" Version="18.1.0" /> <PackageReference Include="YamlDotNet" Version="18.1.0" />
+4
View File
@@ -132,7 +132,11 @@ builder.Services.AddSingleton<IMeetingSummaryRetryRunner, MeetingSummaryRetryRun
builder.Services.AddSingleton<IMeetingWorkflowRulesProvider, FileMeetingWorkflowRulesProvider>(); builder.Services.AddSingleton<IMeetingWorkflowRulesProvider, FileMeetingWorkflowRulesProvider>();
builder.Services.AddSingleton<IMeetingWorkflowEngine, MeetingWorkflowEngine>(); builder.Services.AddSingleton<IMeetingWorkflowEngine, MeetingWorkflowEngine>();
builder.Services.AddSingleton<IWorkflowRulesEditorInstructionBuilder, WorkflowRulesEditorInstructionBuilder>(); builder.Services.AddSingleton<IWorkflowRulesEditorInstructionBuilder, WorkflowRulesEditorInstructionBuilder>();
#if WINDOWS
builder.Services.AddSingleton<IWorkflowRulesEditorSamplePlaybackQueue, WorkflowRulesEditorSamplePlaybackQueue>(); builder.Services.AddSingleton<IWorkflowRulesEditorSamplePlaybackQueue, WorkflowRulesEditorSamplePlaybackQueue>();
#else
builder.Services.AddSingleton<IWorkflowRulesEditorSamplePlaybackQueue, UnavailableWorkflowRulesEditorSamplePlaybackQueue>();
#endif
builder.Services.AddSingleton<IWorkflowRulesEditorChatPipeline, WorkflowRulesEditorChatPipeline>(); builder.Services.AddSingleton<IWorkflowRulesEditorChatPipeline, WorkflowRulesEditorChatPipeline>();
builder.Services.AddTransient<WorkflowRulesEditorChatViewModel>(); builder.Services.AddTransient<WorkflowRulesEditorChatViewModel>();
#if WINDOWS #if WINDOWS
@@ -805,7 +805,9 @@ public sealed class MeetingRecordingCoordinator
run.RecordTranscriptActivity(segment, inactivityClock.Now); run.RecordTranscriptActivity(segment, inactivityClock.Now);
if (!string.IsNullOrWhiteSpace(segment.Text)) 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, RecordingRun run,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await gate.WaitAsync(cancellationToken);
try 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.
} }
} }
@@ -8,6 +8,7 @@ public interface IWorkflowRulesEditorSamplePlaybackQueue
Task<string> QueueAsync(int sampleId, byte[] wavBytes, CancellationToken cancellationToken = default); Task<string> QueueAsync(int sampleId, byte[] wavBytes, CancellationToken cancellationToken = default);
} }
#if WINDOWS
public sealed class WorkflowRulesEditorSamplePlaybackQueue : IWorkflowRulesEditorSamplePlaybackQueue, IDisposable public sealed class WorkflowRulesEditorSamplePlaybackQueue : IWorkflowRulesEditorSamplePlaybackQueue, IDisposable
{ {
private readonly Channel<QueuedSample> channel = Channel.CreateUnbounded<QueuedSample>(); private readonly Channel<QueuedSample> channel = Channel.CreateUnbounded<QueuedSample>();
@@ -98,3 +99,13 @@ public sealed class WorkflowRulesEditorSamplePlaybackQueue : IWorkflowRulesEdito
private sealed record QueuedSample(int SampleId, byte[] WavBytes); private sealed record QueuedSample(int SampleId, byte[] WavBytes);
} }
#else
public sealed class UnavailableWorkflowRulesEditorSamplePlaybackQueue : IWorkflowRulesEditorSamplePlaybackQueue
{
public Task<string> QueueAsync(int sampleId, byte[] wavBytes, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult("Speaker sample playback requires the Windows build of Meeting Assistant.");
}
}
#endif
+2
View File
@@ -1,5 +1,7 @@
# Meeting Workflow Engine # Meeting Workflow Engine
The interactive rules and identities editor can queue speaker samples for playback in the Windows build. The neutral build reports that playback requires the Windows build; it does not enqueue audio or initialize Windows audio devices.
Meeting Assistant has a small local workflow engine for meeting-specific automation. It is intended for rules that are too personal or environment-specific to hard-code, such as adding default attendees, cleaning meeting titles, binding projects, or adding context notes when known speakers are detected. Meeting Assistant has a small local workflow engine for meeting-specific automation. It is intended for rules that are too personal or environment-specific to hard-code, such as adding default attendees, cleaning meeting titles, binding projects, or adding context notes when known speakers are detected.
The workflow engine is deliberately narrow. It runs from a local YAML file, evaluates rules against the current meeting note read from disk, and applies a small set of meeting-safe mutations. The workflow engine is deliberately narrow. It runs from a local YAML file, evaluates rules against the current meeting note read from disk, and applies a small set of meeting-safe mutations.
@@ -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. 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 ### 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. 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.
@@ -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. 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. 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. 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 - **WHEN** the older meeting writes a late transcript segment
- **THEN** the newer meeting's notification and pending actions remain active - **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 #### Scenario: Empty stopped recording is cleaned up
- **GIVEN** a recording is active - **GIVEN** a recording is active
- **AND** the meeting note, transcript, and assistant context only contain generated default content - **AND** the meeting note, transcript, and assistant context only contain generated default content
@@ -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.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.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. - [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.
+8
View File
@@ -285,6 +285,8 @@ The rules and identities editor agent SHALL receive speaker identity tools to se
The rules and identities editor agent SHALL receive speaker sample tools to list, read, delete, and queue playback of samples linked to identities. The delete sample tool SHALL refuse to delete the last remaining sample for an identity. The rules and identities editor agent SHALL receive speaker sample tools to list, read, delete, and queue playback of samples linked to identities. The delete sample tool SHALL refuse to delete the last remaining sample for an identity.
Speaker sample playback SHALL use the Windows audio implementation only in the Windows-targeted build. The neutral build SHALL return an actionable unavailable response instead of reporting a sample as queued for playback.
The first model request caused by each user-submitted chat turn SHALL send the `X-Initiator: user` header, while follow-up model requests within that same turn, such as tool-call continuations, SHALL send `X-Initiator: agent`. The first model request caused by each user-submitted chat turn SHALL send the `X-Initiator: user` header, while follow-up model requests within that same turn, such as tool-call continuations, SHALL send `X-Initiator: agent`.
Meeting Assistant SHALL provide a diagnostic endpoint that opens the workflow rules editor through the same window service used by the tray menu. Meeting Assistant SHALL provide a diagnostic endpoint that opens the workflow rules editor through the same window service used by the tray menu.
@@ -337,6 +339,12 @@ Meeting Assistant SHALL provide a diagnostic endpoint that opens the workflow ru
- **AND** it can list, read, delete, and queue playback of identity samples - **AND** it can list, read, delete, and queue playback of identity samples
- **AND** deleting the last sample for an identity is refused - **AND** deleting the last sample for an identity is refused
#### Scenario: Neutral build refuses speaker playback
- **GIVEN** Meeting Assistant runs from its neutral target build
- **WHEN** an agent requests playback of a speaker sample
- **THEN** the application reports that playback requires the Windows build
- **AND** does not report that the sample was queued
#### Scenario: User sends a rules-editing chat turn #### Scenario: User sends a rules-editing chat turn
- **GIVEN** the rules editor chat window is open - **GIVEN** the rules editor chat window is open
- **WHEN** the user types a prompt and presses Enter - **WHEN** the user types a prompt and presses Enter