跳转至

P5.UA13-realtime-session-service · ADR-13 RealtimeSessionService 实施(议题 3.2 工程态隔离 + realtime/xilink 互斥)

Worker:ClaudeB · 后端 (backend-csharp) / 预计:1.0d / 优先级:P1 / 状态:dispatched 隔离:🧵 文件隔离(同 worktree 同 branch · 与 ADR-13 fork 2 + fork 3 + ADR-09 4 active fork 文件正交)


🔍 触发与解锁链

触发 状态 影响
ADR-AIOS-13 v1.0 accepted(2026-06-01 19:21) ✅ user accept 8 fork ready · 用户首批 start fork 1+6
真值核查 #1(subagent · 2026-06-01 18:25) ✅ AudioDeviceService 已就位 · sidecar 0 I/O · 单 engine instance 本 fork 实施"独立 session 不动 user project"
ADR-12 fulfilled 🏆 ✅ 5ea9806 收尾 7 类 MeasurementNode + WS 推送基础设施已就位 · 本 fork 复用

→ 本 fork zombie 解锁:Phase 2 fork 4(realtimeRunStore + 顶栏)+ fork 5(input/output device 面板)· 总 1.5d。


任务定义(基于 ADR-AIOS-13 v1.0 §2.4 + §3.3 + §11)

新建 RealtimeSessionService 独立 session 管理服务,实现 ADR-13 议题 3 决议:

核心范围: 1. RealtimeSessionService 单例(独立 session ID = xitest-realtime · 不写 audioEngineService 内部状态) 2. REST API:POST /api/realtime/start + POST /api/realtime/stop + GET /api/realtime/state 3. WS 推送:WS /ws/realtime/state 推送状态变化(heartbeat ≥ 500ms · state change 立即推) 4. 与 xilink engine 互斥(用户拍板 Q3.1=A · ADR §2.4):start realtime 时若 audioEngineService.IsRunning → 自动 stop xilink → 返 previousSessionStopped 字段 5. Ximind 兼容性 5 项(ADR §11):自描述 JSON / description 字段 / recovery_hints[] / 审计日志 data/realtime_test_projects/_audit.jsonl / 结构化 RealtimeError

不在本 fork 范围(留 fork ⅔): - ❌ 不实施 BuiltinLinkRegistry(fork 2 · Services/Link/BuiltinLinks/) - ❌ 不修改 AudioDeviceService.cs(fork 3 · 加 bypass-xilink 模式) - ❌ 不写 /ws/realtime/stream PCM 推流(fork 3 · 复用 ADR-12 #8 toolKind 路由) - ❌ 不写 sidecar 端任何代码(本 ADR 全程 sidecar 0 改动) - ❌ 不动 contract-v1.0(realtime 走 v2 命名空间)


完整 prompt(直接复制粘贴 worker 终端)

[U-thread]   P5.UA13-realtime-session-service
[部门]       后端 (backend-csharp) · 推荐 skill: dotnet-realtime-communication
[Worker CWD] d:/work/25_claude/workspace/AlgoDepartment/04_development/
[Occupies]   P5.K-services-realtime(写 · 新建 Services/Realtime/ 命名空间) + P5.K-services-engine(read · 互斥逻辑读 audioEngineService.IsRunning)
[隔离]       🧵 文件隔离 · 仅写:
             - backend_csharp/Services/Realtime/IRealtimeSessionService.cs(新建)
             - backend_csharp/Services/Realtime/RealtimeSessionService.cs(新建)
             - backend_csharp/Models/Realtime/RealtimeSessionState.cs(新建 record)
             - backend_csharp/Models/Realtime/StartRealtimeRequest.cs(新建 record)
             - backend_csharp/Models/Realtime/RealtimeError.cs(新建 record · Ximind 兼容)
             - backend_csharp/Routes/RealtimeApiRoutes.cs(新建)
             - backend_csharp/Routes/RealtimeWsRoutes.cs(新建)
             - backend_csharp/Program.cs(扩展:注册 service + map 2 路由 · 行号正交 fork 2/3)
             - backend_csharp/Tests/Services/Realtime/RealtimeSessionServiceTests.cs(新建 xunit)
             与 ADR-13 fork 2 (Services/Link/BuiltinLinks/) + fork 3 (Services/Meter/AudioDeviceService.cs 仅扩展) 文件路径完全正交
             与 ADR-09 4 active fork (Services/Plugin/ 等) 文件路径完全正交
[优先级]     P1 · 1.0d · 关键路径 · 解锁 Phase 2 fork 4+5
[ADR]        d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/ADR/ADR-AIOS-13-xitest-realtime-dual-mode.md
             (必读 §2.4 RealtimeSessionService + §2.10 边界铁律 4 互斥 + §3.3 议题 3 业务行为契约 + §11 Ximind 兼容性 5 项)
[业务行为契约引用] ADR-13 §3.3(① 输入输出契约 + ② 收敛/成功判据 + ③ 失败回退 + ④ 用户操作流 + ⑤ e2e 真值)
[参考文档](绝对路径 · worker 必读)
  - ADR-13:d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/ADR/ADR-AIOS-13-xitest-realtime-dual-mode.md
    · §2.4 RealtimeSessionService 完整定义(record schema + 互斥规则)
    · §2.10 边界铁律 第 4 项(realtime 与 xilink engine 互斥)
    · §3.3 议题 3 业务行为契约(5 子段)
    · §10 6 议题矩阵(议题 3.1 顶栏单按钮 + 议题 3.2 工程态隔离 → 本 fork 主战场)
    · §11.5 RealtimeError 结构(code/message/human_readable_message/recovery_hints[]/context · 必填段)
  - PCB:d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/processes/P_arch/ADR-AIOS-13/PROCESS.md
  - 范式参考(已 zombie · ClaudeB 同部门标本):
    · d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/prompts/done/P5.U-runtime-mode-refactor--b43c35a.md
      (RuntimeTargetService 单例 + REST + WS handler 范式 · 7 预定义 + LEGACY_MAP)
    · d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/prompts/done/P5.U-meter-tap-multi-tool--48cf0ba.md
      (MeterTapService + 7 toolKind 路由 + WS 推送范式 · fork 3 将复用)
  - 现状必读(主仓真值核查 · Step 1 必跑):
    · backend_csharp/Services/AudioEngine/AudioEngineService.cs(必看 IsRunning 属性 + StartAsync/StopAsync 接口 · 本 fork 互斥逻辑读它)
    · backend_csharp/Services/Meter/AudioDeviceService.cs(必看 EnumerateDevices · 本 fork InputDeviceId 校验需调它)
    · backend_csharp/Program.cs(必看 service 注册顺序 + WebApplication.MapXxx 路由风格 · 模仿现有风格新增 RealtimeApiRoutes/RealtimeWsRoutes 注册)
    · backend_csharp/Routes/MeterDevApiRoutes.cs(WS 推送范式 · 本 fork RealtimeWsRoutes 模仿)

【背景】
ADR-AIOS-13 v1.0 已 accepted(2026-06-01 19:21)。议题 3 决议:realtime 模式与 xilink user project 完全独立 · 同时只能 1 个 session active(audio device 抢占规避)。

3 路 subagent 主仓真值核查(2026-06-01 18:25)发现:
  ① C# AudioEngineService 当前是单 engine instance · 直接读 user project · 没"运行独立 session 不动 project state"机制
  ② 前端 xitest 不读 xilink project(天然解耦 · 改造点小)
  ③ 没有 RealtimeSessionService(本 fork 新建)

本 fork 核心:用 session ID 区分 xitest-realtime vs xilink user project · 二者互斥 start。

【执行步骤】(7 步 · 总 1.0d)

Step 1 · 真值核查 + git pull(必跑 · 0.05d)
  - git status / git pull origin xistudio --no-rebase
  - cat backend_csharp/Services/AudioEngine/AudioEngineService.cs | head -100
    · 输出 IsRunning 属性签名 + StartAsync/StopAsync 签名 · 互斥逻辑要调用
  - cat backend_csharp/Program.cs | head -80
    · 输出 service 注册段(builder.Services.AddSingleton<...>) + map 路由段(app.Map...)的现有风格
  - grep -r "AddSingleton" backend_csharp/Program.cs · 看 audioEngineService 注册方式 · 模仿
  - 输出真值核查到 commit message body:
    · audioEngineService 当前 IsRunning 签名:`???`
    · StopAsync 签名:`???`
    · Program.cs 路由注册风格:`???`

Step 2 · 落 5 个 model record(0.15d)
  - backend_csharp/Models/Realtime/RealtimeSessionState.cs:
    public record RealtimeSessionState(
        string SessionId,           // 永远 = "xitest-realtime"
        string Status,              // "idle" | "starting" | "running" | "stopping" | "error"
        string? Mode,               // "hardware" | "loopback" | null(idle 时)
        string? InputDeviceId,
        string? OutputDeviceId,
        SignalConfig? SignalConfig, // null when mode=hardware
        RealtimeError? Error,
        long UpdatedAt              // unix ms
    );
    public record SignalConfig(string Type, double? Frequency, double AmplitudeDbfs, string? Description);

  - backend_csharp/Models/Realtime/StartRealtimeRequest.cs:
    public record StartRealtimeRequest(
        string Mode,                // "hardware" | "loopback"
        string InputDeviceId,
        string ToolKind,            // "fft"|"rms"|"transfer"|"phase"|"waveform"|"electrical"|"recorder"
        int[] Channels,
        int SampleRate,
        int? FftSize,
        string? OutputDeviceId,     // loopback only
        SignalConfig? SignalConfig, // loopback only
        string? Description         // Ximind 自然语言描述(可选)
    );

  - backend_csharp/Models/Realtime/RealtimeError.cs(ADR §11.5 结构):
    public record RealtimeError(
        string Code,                // 枚举 8 个见下
        string Message,             // EN
        string HumanReadableMessage,// 中文
        string[] RecoveryHints,
        Dictionary<string, object>? Context
    );
    // Code 枚举(常量 class):AUDIO_DEVICE_BUSY / AUDIO_DEVICE_NOT_FOUND / XILINK_STOP_FAILED /
    //                       BUILTIN_LINK_LOAD_FAILED / SIDECAR_UNAVAILABLE / INVALID_REQUEST /
    //                       SESSION_ALREADY_RUNNING / DEVICE_LOOPBACK_SAME_DEVICE

  - StartResult / StopResult record(API response):见 step 3 注释

Step 3 · 落 IRealtimeSessionService + RealtimeSessionService(0.3d)
  - IRealtimeSessionService.cs:
    public interface IRealtimeSessionService
    {
        bool IsRunning { get; }
        RealtimeSessionState GetState();
        Task<StartResult> StartAsync(StartRealtimeRequest req, CancellationToken ct = default);
        Task<StopResult> StopAsync(CancellationToken ct = default);
        event Action<RealtimeSessionState>? OnStateChanged;  // WS handler 订阅
    }
    public record StartResult(bool Success, RealtimeSessionState State, PreviousSessionStopped? PreviousSessionStopped, RealtimeError? Error);
    public record StopResult(bool Success, RealtimeSessionState State, RealtimeError? Error);
    public record PreviousSessionStopped(string Type, string? ProjectId);  // type="xilink"

  - RealtimeSessionService.cs 关键逻辑:
    · ctor 注入 IAudioEngineService(read · 互斥) + IAudioDeviceService(read · device 校验) + ILogger
    · _state 字段 · lock _stateLock 保护并发 start/stop
    · IsRunning => _state.Status == "running"
    · GetState() => _state(deep clone or record copy)
    · StartAsync:
        ① 校验 req.Mode in {"hardware","loopback"} · req.InputDeviceId 存在(_audioDeviceService.GetDeviceById 校验)· loopback 时 outputDeviceId != inputDeviceId
        ② 若 _state.Status == "running" → 返 RealtimeError(SESSION_ALREADY_RUNNING) · 不动状态
        ③ 若 _audioEngineService.IsRunning → 调 _audioEngineService.StopAsync(ct) · 失败返 XILINK_STOP_FAILED · 成功记录 PreviousSessionStopped(Type="xilink", ProjectId=current)
        ④ 状态切 starting → running · 记录 mode/inputDeviceId/outputDeviceId/signalConfig · 写审计日志
        ⑤ 触发 OnStateChanged 事件 · 返 StartResult
    · StopAsync:
        ① 若 IsRunning == false → 返 success(idempotent · 不报错)
        ② 状态切 stopping → idle · 清理 mode 等字段
        ③ 写审计日志 · 触发 OnStateChanged · 返 StopResult
    · 审计日志路径:`data/realtime_test_projects/_audit.jsonl`(append-only · 不存在则创建目录 · ADR §11.4)
        format:{"ts":<ms>,"event":"realtime.start"|"realtime.stop","sessionId":"xitest-realtime","mode":"...","inputDeviceId":"...","previousSessionStopped":{...}}

Step 4 · 落 RealtimeApiRoutes(REST · 0.15d)
  - backend_csharp/Routes/RealtimeApiRoutes.cs:
    public static class RealtimeApiRoutes
    {
        public static void MapRealtimeApi(this WebApplication app)
        {
            app.MapPost("/api/realtime/start", async (StartRealtimeRequest req, IRealtimeSessionService svc) =>
            {
                var r = await svc.StartAsync(req);
                return r.Success ? Results.Ok(r) : Results.BadRequest(r);
            });
            app.MapPost("/api/realtime/stop", async (IRealtimeSessionService svc) =>
            {
                var r = await svc.StopAsync();
                return Results.Ok(r);
            });
            app.MapGet("/api/realtime/state", (IRealtimeSessionService svc) =>
                Results.Ok(svc.GetState()));
        }
    }
  - Program.cs 加 1 行 `app.MapRealtimeApi();`(模仿现有 MeterDevApi 风格 · 行号正交 fork 2/3 加的注册行)

Step 5 · 落 RealtimeWsRoutes(WS · 0.15d)
  - backend_csharp/Routes/RealtimeWsRoutes.cs:
    public static class RealtimeWsRoutes
    {
        public static void MapRealtimeWs(this WebApplication app)
        {
            app.Map("/ws/realtime/state", async (HttpContext ctx, IRealtimeSessionService svc) =>
            {
                if (!ctx.WebSockets.IsWebSocketRequest) { ctx.Response.StatusCode = 400; return; }
                using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
                // 1) 接受连接立即推当前 state(snapshot)
                await SendStateAsync(ws, svc.GetState(), ctx.RequestAborted);
                // 2) 订阅 OnStateChanged · 推送变化(用 Channel<T> 缓冲)
                var channel = System.Threading.Channels.Channel.CreateUnbounded<RealtimeSessionState>();
                Action<RealtimeSessionState> handler = s => channel.Writer.TryWrite(s);
                svc.OnStateChanged += handler;
                try
                {
                    await foreach (var s in channel.Reader.ReadAllAsync(ctx.RequestAborted))
                        await SendStateAsync(ws, s, ctx.RequestAborted);
                }
                finally { svc.OnStateChanged -= handler; }
            });
        }
        private static Task SendStateAsync(WebSocket ws, RealtimeSessionState state, CancellationToken ct) { ... JSON serialize + SendAsync ... }
    }
  - Program.cs 加 1 行 `app.MapRealtimeWs();`

Step 6 · 落 xunit 测试(0.15d)
  - backend_csharp/Tests/Services/Realtime/RealtimeSessionServiceTests.cs · 8 case:
    · GetState_Initial_ReturnsIdle
    · StartAsync_Hardware_TransitionsToRunning_AndEmitsEvent
    · StartAsync_Loopback_RequiresOutputDeviceId
    · StartAsync_LoopbackSameDevice_ReturnsError(code=DEVICE_LOOPBACK_SAME_DEVICE)
    · StartAsync_WhenAlreadyRunning_ReturnsError(code=SESSION_ALREADY_RUNNING)
    · StartAsync_WhenXilinkRunning_StopsXilinkFirst_AndReturnsPreviousSessionStopped
    · StopAsync_WhenIdle_IsIdempotent
    · StopAsync_WhenRunning_TransitionsToIdle_AndEmitsEvent
  - 用 Mock<IAudioEngineService> + Mock<IAudioDeviceService> 隔离依赖

Step 7 · build + test + 手动 e2e + commit(0.05d)
  - cd backend_csharp && dotnet build(零错误零警告 · 基线 217/0)
  - dotnet test(基线 +8 用例 · 217 → 225)
  - 手动 e2e(若 sln 可启动):
    · dotnet run · curl http://localhost:5000/api/realtime/state → idle 200 OK
    · curl -X POST http://localhost:5000/api/realtime/start -H "Content-Type: application/json" -d '{"mode":"hardware","inputDeviceId":"<some-real-device>","toolKind":"fft","channels":[0],"sampleRate":48000}' → 200 + state.status="running"
    · curl -X POST http://localhost:5000/api/realtime/stop → 200 + state.status="idle"
    · 注:若 inputDeviceId 校验失败(无真硬件)是预期 · 此时 fork 3 zombie 后才有 device 真值校验
  - git add backend_csharp/Services/Realtime/ backend_csharp/Models/Realtime/ backend_csharp/Routes/RealtimeApiRoutes.cs backend_csharp/Routes/RealtimeWsRoutes.cs backend_csharp/Program.cs backend_csharp/Tests/Services/Realtime/
  - git commit(三元组 trailer 见下) · git push origin xistudio

【验收】

形式合规:
- [ ] dotnet build 零错误零警告
- [ ] dotnet test 全绿(基线 217/0 → 225/0 · +8 用例)
- [ ] 仅 9 文件修改:5 model + 1 interface + 1 service + 2 routes(API + WS)+ Program.cs(注册 2 行)+ 1 测试文件(共 ≤ 10 文件)
- [ ] 不动 contract-v1.0 / AudioDeviceService 内核(fork 3 才扩)/ AudioEngineService(只读)
- [ ] 不动 Services/Link/(fork 2 范围)
- [ ] 0 引入新 NuGet 包(若必须用 System.Threading.Channels 已在 .NET 运行时 · 无需新增)

业务行为契约(端到端真值 · 必跑 · ADR §3.3):
- [ ] curl GET /api/realtime/state(idle 时)→ 200 · status="idle" · sessionId="xitest-realtime"
- [ ] curl POST /api/realtime/start(hardware mode)→ 200 · status="running" · mode="hardware"
- [ ] curl POST /api/realtime/start(再次 · already running)→ 400 + RealtimeError(code=SESSION_ALREADY_RUNNING · recovery_hints 含 "stop current session first")
- [ ] curl POST /api/realtime/stop → 200 · status="idle"
- [ ] WS /ws/realtime/state 连接后立即收到 1 帧 snapshot · start/stop 时各推 1 帧
- [ ] 审计日志 data/realtime_test_projects/_audit.jsonl 每次 start/stop 各 append 1 行

Ximind 兼容性(ADR §11 · 必跑):
- [ ] RealtimeSessionState 字段名自描述(不是缩写)· JSON 序列化用 camelCase
- [ ] StartRealtimeRequest 含可选 description 字段(自然语言描述测量目的)
- [ ] RealtimeError 含 code + message + human_readable_message + recovery_hints[](≥ 1 项每个 error · 必含)
- [ ] 审计日志路径正确(data/realtime_test_projects/_audit.jsonl · 路径不存在自动创建目录)

【commit】
subject:`feat(P5.UA13-realtime-session-service): RealtimeSessionService + REST/WS API + xilink mutex · ADR-13 §2.4 §3.3 §11`

trailer(必须精确 · 三元组):
[step=7/7] [pid=P5] [uid=UA13-realtime-session-service] [occupies=P5.K-services-realtime+P5.K-services-engine(read)]
[files=Services/Realtime/IRealtimeSessionService.cs, Services/Realtime/RealtimeSessionService.cs, Models/Realtime/RealtimeSessionState.cs, Models/Realtime/StartRealtimeRequest.cs, Models/Realtime/RealtimeError.cs, Routes/RealtimeApiRoutes.cs, Routes/RealtimeWsRoutes.cs, Program.cs, Tests/Services/Realtime/RealtimeSessionServiceTests.cs]
[isolation] file(同 worktree 同 branch · 与 ADR-13 fork 2/3 + ADR-09 4 active 文件路径完全正交)
[derived_from] ADR-AIOS-13 v1.0 §2.4 + §3.3 + §11
[truth-check] AudioEngineService.IsRunning 签名=??? · StopAsync 签名=??? · Program.cs 注册风格=???(Step 1 输出真值)
[acceptance] curl /api/realtime/{state,start,stop} 4 用例全过 · WS /ws/realtime/state 推送验证 · xunit 8 用例全绿
[ximind] 5 项检查清单全过(自描述字段 · description 字段 · recovery_hints · 审计日志 · 结构化 error)

【禁止】(7 条红线)
1. ❌ 不修改 backend_csharp/Services/AudioEngine/AudioEngineService.cs(只读 IsRunning + StopAsync · 不能动其内部)
2. ❌ 不修改 backend_csharp/Services/Meter/AudioDeviceService.cs(fork 3 范围 · 本 fork 仅读 GetDeviceById 校验)
3. ❌ 不写 BuiltinLinkRegistry / Services/Link/BuiltinLinks/(fork 2 范围)
4. ❌ 不写 /ws/realtime/stream PCM 推流(fork 3 范围 · 本 fork 仅 /ws/realtime/state 元数据推送)
5. ❌ 不修改 contract-v1.0 任何字段(realtime 走 v2 命名空间 · 后续 ADR-14 才考虑)
6. ❌ 不省略 Step 1 真值核查报告(commit body 必含 AudioEngineService 实际签名)
7. ❌ 不省略 RealtimeError.recovery_hints[](Ximind 铁律 · 大模型基于此自主重试)

解锁链(本任务 zombie 后)

  • ✅ Phase 2 fork 4 P0.UA13-realtime-run-store-toolbar(等本 fork + fork 3 zombie · realtimeRunStore 调 /api/realtime/start)
  • ✅ Phase 2 fork 5 P0.UA13-input-output-device-config-panel(等本 fork + fork 2 zombie · 调 /api/builtin-links + /api/realtime/start)
  • ✅ Phase 3 fork 8 P_e2e.UA13-truth(议题 3.2 互斥切换 e2e 验收)
  • ✅ ADR-13 §3.3 议题 3 业务行为契约 ⑤ e2e 真值脚本可跑

风险评估

风险 缓解
⚠️ AudioEngineService.IsRunning 当前签名可能与 ADR §2.4 假设不符 Step 1 真值核查必跑 · grep 出真实签名后再实施 · 若不一致(eg. 异步 IsRunningAsync)→ commit body 报告 + 适配
⚠️ WS Channel 在高并发 start/stop 时 race condition _stateLock 锁住状态切换 · OnStateChanged 在 lock 外触发(避免 deadlock)· Channel 单线程 writer 安全
⚠️ 审计日志目录 data/realtime_test_projects/ 不存在 StartAsync 内 Directory.CreateDirectory(若不存在)· 不依赖 fork 6 storage 落盘(那是前端 IDB)
⚠️ xilink stop 失败导致 realtime 卡 starting 超时 5s · 失败返 XILINK_STOP_FAILED + recovery_hints=["manually stop xilink in xilink stage"] · 状态回 idle
⚠️ 与 fork ⅔ 同 PR 合并冲突 Program.cs 三 fork 都改 Program.cs 但行号正交(本 fork 加 MapRealtimeApi/Ws · fork 2 加 MapBuiltinLinkApi · fork 3 不动 Program.cs)· commit 时间错开 · git pull 自动合并

历史

时间 事件 hash
2026-06-01 19:30 dispatched · 用户首批 start 4 fork 中关键路径 fork 1 · ADR-13 v1.0 accepted 9 分钟后派发 · ClaudeB 1.0d 🧵 file 与 fork ⅔ + ADR-09 4 active 文件正交 · 解锁 Phase 2 fork 4+5