forked from Manuel/meeting-assistant
Merge remote-tracking branch 'refs/remotes/fork/codex/macos-support' into codex/macos-support
# Conflicts: # MeetingAssistant/Program.cs # README.md
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using MeetingAssistant.Recording;
|
||||
using MeetingAssistant.Workflow;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
@@ -54,6 +55,17 @@ public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Pr
|
||||
Assert.Equal(HostOperatingSystem.DetectCurrent(), first);
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task ApplicationStartupDeletesStaleTemporaryRecordings()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using MeetingAssistant.Transcription;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
@@ -170,7 +171,7 @@ public sealed class MacOsMeetingAudioSourceTests
|
||||
microphonePcm: Pcm16(2_000),
|
||||
systemPcm: Pcm16(10_000));
|
||||
var speechPipelines = new CapturingSpeechRecognitionPipelineFactory();
|
||||
await using var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
|
||||
var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
@@ -225,6 +226,9 @@ public sealed class MacOsMeetingAudioSourceTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
await factory.DisposeAsync();
|
||||
using var connection = new SqliteConnection($"Data Source={Path.Combine(testRoot, "speakers.db")}");
|
||||
SqliteConnection.ClearPool(connection);
|
||||
if (Directory.Exists(testRoot))
|
||||
{
|
||||
Directory.Delete(testRoot, recursive: true);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2432,12 +2432,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"));
|
||||
@@ -2452,13 +2457,16 @@ public sealed class RecordingCoordinatorTests
|
||||
new CapturingMeetingSummaryPipeline(),
|
||||
Options.Create(launchProfiles.GetRequiredProfile(null).Options),
|
||||
NullLogger<MeetingRecordingCoordinator>.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);
|
||||
|
||||
@@ -2466,10 +2474,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]
|
||||
@@ -5186,6 +5205,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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<MicrosoftSpeechVersion>1.51.1</MicrosoftSpeechVersion>
|
||||
<MicrosoftSpeechVersion>1.51.2</MicrosoftSpeechVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
@@ -41,16 +41,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.20.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.21.0" />
|
||||
<PackageReference Include="DiffPlex" Version="1.9.0" />
|
||||
<PackageReference Include="Microsoft.CognitiveServices.Speech" Version="$(MicrosoftSpeechVersion)" />
|
||||
<PackageReference Include="Microsoft.CognitiveServices.Speech.Extension.MAS" Version="$(MicrosoftSpeechVersion)" ExcludeAssets="build" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
|
||||
<PackageReference Include="NAudio" Version="2.3.0" />
|
||||
<PackageReference Include="NCalcSync" Version="7.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />
|
||||
<PackageReference Include="NAudio" Version="3.1.0" />
|
||||
<PackageReference Include="NCalcSync" Version="7.2.0" />
|
||||
<PackageReference Include="RazorLight" Version="2.3.1" />
|
||||
<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.Runtime" Version="1.9.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="18.1.0" />
|
||||
|
||||
@@ -17,11 +17,11 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Logging.AddProvider(new MeetingAssistantFileLoggerProvider());
|
||||
builder.Services.AddSingleton(HostOperatingSystem.DetectCurrent());
|
||||
builder.Services.AddSingleton<IValidateOptions<MeetingAssistantOptions>, MeetingAssistantSpeakerSampleOptionsValidator>();
|
||||
builder.Services.AddOptions<MeetingAssistantOptions>()
|
||||
.Bind(builder.Configuration.GetSection("MeetingAssistant"))
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddSingleton(HostOperatingSystem.DetectCurrent());
|
||||
builder.Services.AddSingleton<ILaunchProfileOptionsProvider, ConfigurationLaunchProfileOptionsProvider>();
|
||||
#if WINDOWS
|
||||
builder.Services.AddSingleton<MicrophoneDeviceSelection>();
|
||||
@@ -129,7 +129,11 @@ builder.Services.AddSingleton<IMeetingSummaryRetryRunner, MeetingSummaryRetryRun
|
||||
builder.Services.AddSingleton<IMeetingWorkflowRulesProvider, FileMeetingWorkflowRulesProvider>();
|
||||
builder.Services.AddSingleton<IMeetingWorkflowEngine, MeetingWorkflowEngine>();
|
||||
builder.Services.AddSingleton<IWorkflowRulesEditorInstructionBuilder, WorkflowRulesEditorInstructionBuilder>();
|
||||
#if WINDOWS
|
||||
builder.Services.AddSingleton<IWorkflowRulesEditorSamplePlaybackQueue, WorkflowRulesEditorSamplePlaybackQueue>();
|
||||
#else
|
||||
builder.Services.AddSingleton<IWorkflowRulesEditorSamplePlaybackQueue, UnavailableWorkflowRulesEditorSamplePlaybackQueue>();
|
||||
#endif
|
||||
builder.Services.AddSingleton<IWorkflowRulesEditorChatPipeline, WorkflowRulesEditorChatPipeline>();
|
||||
builder.Services.AddTransient<WorkflowRulesEditorChatViewModel>();
|
||||
#if WINDOWS
|
||||
|
||||
@@ -823,7 +823,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -831,6 +833,8 @@ public sealed class MeetingRecordingCoordinator
|
||||
private async Task DismissInactivityPromptsForRunAsync(
|
||||
RecordingRun run,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
@@ -847,6 +851,11 @@ public sealed class MeetingRecordingCoordinator
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Pending cleanup is no longer needed after this run stops capturing.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DismissInactivityPromptsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Text.Json.Nodes;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace MeetingAssistant.Summary;
|
||||
@@ -798,7 +797,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
string apiKey,
|
||||
AsyncLocal<string?> requestInitiator)
|
||||
{
|
||||
var options = new OpenAIClientOptions
|
||||
var options = new ResponsesClientOptions
|
||||
{
|
||||
Endpoint = httpClient.BaseAddress
|
||||
?? throw new InvalidOperationException("LiteLLM HTTP client requires a base address."),
|
||||
|
||||
@@ -8,6 +8,7 @@ public interface IWorkflowRulesEditorSamplePlaybackQueue
|
||||
Task<string> QueueAsync(int sampleId, byte[] wavBytes, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
#if WINDOWS
|
||||
public sealed class WorkflowRulesEditorSamplePlaybackQueue : IWorkflowRulesEditorSamplePlaybackQueue, IDisposable
|
||||
{
|
||||
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);
|
||||
}
|
||||
#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
|
||||
|
||||
@@ -61,7 +61,7 @@ Recording can be controlled through global hotkeys, the Windows tray icon, the m
|
||||
- `Ctrl+Alt+Z`: abort the active run and delete its artifacts.
|
||||
- `Ctrl+Alt+S`: capture the active window into the meeting context.
|
||||
|
||||
The Windows tray and macOS menu-bar menu present `Finish meeting` as the primary action during capture; cancel/discard and profile switching remain separate controls. The Windows tray additionally exposes pause/unpause transcription and microphone selection. A paused meeting stays active and can still be finished or canceled normally. `Exit` is always available and requires confirmation while any meeting is recording or processing.
|
||||
The Windows tray presents `Finish meeting` as the primary action during capture; microphone selection, cancel/discard, profile switching, and pause/unpause transcription remain separate controls. A paused meeting stays active and can still be finished or canceled normally. Windows `Exit` requires confirmation while any meeting is recording or processing. The macOS menu offers profile start actions while idle and `Stop meeting recording and transcribe` plus cancel/discard during capture; its `Exit` action currently exits directly.
|
||||
|
||||
The loopback HTTP surface has no application authentication, so port `5090` must remain a trusted loopback-only control surface. The main endpoints are:
|
||||
|
||||
@@ -110,7 +110,7 @@ Abort is destructive: it removes the active run's note, transcript, context, sum
|
||||
|
||||
- The default `azure-speech` provider sends mixed meeting audio and dictation phrase hints to Azure AI Speech. Azure-backed speaker matching also sends selected voice audio.
|
||||
- Summary, screenshot OCR, and interactive-agent requests go to the configured OpenAI-compatible Responses endpoint. They can include meeting/transcript/project text, screenshots, configuration, logs, and speaker samples when corresponding tools are used. The checked-in endpoint is a loopback proxy; its ultimate provider, data path, and retention policy are outside this repository.
|
||||
- Outlook Classic access on Windows is local COM and reads appointment metadata. EventKit access on macOS reads calendars synchronized into the Calendar app. Neither provides the primary capture path.
|
||||
- Outlook Classic access on Windows is local COM. EventKit access on macOS reads calendars synchronized into the Calendar app. Neither provides the primary capture path.
|
||||
- A managed FunASR run pulls its configured image, removes any same-named container, starts a disposable container privileged by default, publishes the configured host port, and mounts the model/hotword cache. Pyannote may build a local image and start disposable containers with audio inputs mounted read-only; those paths require Docker Desktop or a compatible Docker CLI. The opt-in Resemblyzer speaker matcher instead provisions an isolated local Python venv with CPU-only dependencies. First use may download images, Python packages, or models from external registries.
|
||||
|
||||
## Configuration
|
||||
@@ -125,7 +125,7 @@ The settings with the largest operational effect are:
|
||||
- `Recording:InactivitySafeguard`: prompts and can auto-finish a run after no new transcript text; it is not an audio-silence detector.
|
||||
- `LaunchProfiles`: overlay named recording/ASR/agent settings and require distinct hotkeys.
|
||||
- `SpeakerIdentification:Resemblyzer:Enabled`: selects the local, managed-Python-venv vector matcher for the whole application; when disabled, the existing WAV/Azure path stays active. Five vectors unlock matching by default without capping retained evidence, and mature profiles use configurable fail-safe density clustering to remove likely mixed-speaker outliers.
|
||||
- `Automation:RulesPath`: points to the local YAML rules file, normally ignored `meeting-rules.local.yaml`.
|
||||
- `Automation:RulesPath`: points to the local YAML workflow-rules file, normally ignored `meeting-rules.local.yaml`.
|
||||
- `CalendarRecordingPrompts` and `Screenshots`: control Outlook prompts on Windows, EventKit prompts on macOS, capture, attachments, and configured OCR.
|
||||
- `Agent` and `WorkflowRulesEditor`: select the Responses endpoint/model, streaming or non-streaming transport, reasoning, retry, output, and compaction behavior; the available tools are defined by the application.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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`.
|
||||
|
||||
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** 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
|
||||
- **GIVEN** the rules editor chat window is open
|
||||
- **WHEN** the user types a prompt and presses Enter
|
||||
|
||||
Reference in New Issue
Block a user