Skip to content

Commit 4f8a710

Browse files
committed
fix(live-session): 暂停边沿吸收 own_pending 并补发丢采样恢复沿
P2-2:上升沿同稳态先吸收 own_pending 的完成增量(delta_eff>0 才合成), 消除 manual <500ms 往返链上把本已可见暂停重复合成为 Pause/Resume 对。 P2-3:稳态真吸收 own 完成后,若完成区间 resume 晚于已发 Pause 事件时刻 (恢复沿丢采样),补发合成 Resume + flush_cut + 复位 pause_processed, 阻断暂停区间时长虚高与跨边界连句(路径 C)。 测试:+4 真值表用例(上升沿 own_pending delta 1/2、P2-3 分支与对照), flap 稳态用例按新语义更新;全量 app_lib_tests 2327 通过。
1 parent d1f9314 commit 4f8a710

2 files changed

Lines changed: 162 additions & 17 deletions

File tree

‎app/src-tauri/src/live_session_pause.rs‎

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ pub(crate) struct PauseEdgeView {
6363
/// 已消化的区间计数(delta = seq - view.seq 即待判定的漏检区间数)
6464
pub seq: u64,
6565
/// 当前可见暂停区间的完成增量(+1)尚未被吸收(loop 已见上升沿但恢复的
66-
/// Start 尚未执行,或 loop 起点即在暂停中)——对应 +1 到达时吸收一次,
67-
/// 防把"本已可见的暂停"重复合成为漏检区间
66+
/// Start 尚未执行,或 loop 起点即在暂停中)——对应 +1 到达时在稳态/
67+
/// 上升沿/下降沿任一观察吸收一次,防把"本已可见的暂停"重复合成为
68+
/// 漏检区间
6869
pub own_pending: bool,
6970
/// 当前暂停的上升沿是否已处理(未处理=引擎可能未清流——恢复边沿防御 flush)
7071
pub pause_processed: bool,
@@ -124,13 +125,18 @@ fn push_synth(
124125
/// +1,**含 loop 已可见处理的暂停区间**——完成增量须按归属吸收):
125126
/// - own_pending=当前可见暂停的完成增量未吸收:上升沿置真;下降沿 delta 0
126127
/// (恢复抢跑,Start 未执行)保持真;下降沿 delta ≥ 1(自身完成已在
127-
/// delta 内)与稳态吸收(+1 到达)后置假。loop 起点即在暂停中 → new 即真。
128-
/// 上升沿覆盖旧 own_pending(旧期物理未结束——完成增量并入新期下降沿吸收)。
128+
/// delta 内)与稳态/上升沿吸收(own_pending 的 +1 到达)后置假。loop
129+
/// 起点即在暂停中 → new 即真。旧完成增量在上升沿未到(delta 0)→ 待吸收
130+
/// 位并入新期(由新期稳态/下降沿吸收一次)。
129131
/// - 稳态(标志未变):delta 0 → 无;先吸收 own_pending 的 +1,余下 delta 为
130132
/// 双沿都漏检的区间(稳态假=轮询窗内往返;稳态真=暂停期 flap)→ 合成事件
131-
/// 对 + flush_cut(引擎可能跨漏区间连句)
132-
/// - 上升沿(→暂停):可见暂停事件 + flush_cut(no-rescore 断句);delta ≥ 1
133-
/// = 此前的漏检完整区间 → 先补合成对
133+
/// 对 + flush_cut(引擎可能跨漏区间连句);稳态真吸收 own 完成且无余量
134+
/// 时,若完成区间 resume 晚于已发 Pause 事件时刻(可见暂停的恢复沿丢
135+
/// 采样)→ 补发合成 Resume + flush_cut + 复位 pause_processed(P2-3,
136+
/// 见分支注释)
137+
/// - 上升沿(→暂停):可见暂停事件 + flush_cut(no-rescore 断句);先吸收
138+
/// own_pending 的 +1(其恢复沿若已可见发出,不得重复合成),余下
139+
/// delta_eff ≥ 1 = 此前的真漏检完整区间 → 先补合成对
134140
/// - 下降沿(→恢复):可见恢复事件;delta ≥ 1 吸收自身区间完成 +1,余下
135141
/// k = delta-1 为暂停期 flap → 合成 + flush_cut;!pause_processed(loop
136142
/// 起点即在暂停中,引擎未清流)→ 防御 flush(边界保护)
@@ -166,12 +172,42 @@ pub(crate) fn plan_edge_observation(
166172
if delta_eff > 0 {
167173
push_synth(&mut out, &mut next, interval, reason, session_now);
168174
out.flush_cut = true;
175+
} else if paused_now && view.own_pending {
176+
// P2-3(审查修复):吸收的 +1 = 本已可见暂停**自身**区间的完成——
177+
// 物理恢复已发生但 loop 未采到恢复沿(恢复+再次暂停同落一个
178+
// <500ms 未采样窗,该次恢复从未落库,暂停区间会并入下次可见
179+
// 恢复造成时长虚高)。若完成区间 resume 晚于已发出的 Pause
180+
// 事件时刻(存在未见过的恢复沿)→ 补发合成 Resume(时刻夹逼
181+
// ≥ Pause 事件保 DB 单调)+ flush_cut(恢复后采到的内容跨该
182+
// 边界不得与后续连句——路径 C 防护)+ 复位 pause_processed
183+
// (当前暂停=恢复后的再次暂停,其上升沿从未处理——后续下降沿
184+
// k=0 时不再静默放行,防御 flush 兜底)。resume 不晚于 Pause
185+
// 事件时刻 = 更早一次**已见**恢复(抢跑下降沿已发可见 Resume)
186+
// 的迟到完成增量 → 保持现状(静默吸收)。
187+
if let Some((iv, last_p)) =
188+
interval.zip(view.last_pause_ms).filter(|(iv, p)| iv.resume_ms > *p)
189+
{
190+
let r = iv.resume_ms.max(last_p);
191+
out.events.push(PlanEvent {
192+
kind: PlanEventKind::Resume,
193+
source: iv.source,
194+
moment_ms: r,
195+
});
196+
out.flush_cut = true;
197+
next.pause_processed = false;
198+
}
169199
}
170200
} else if paused_now {
171-
// 上升沿:可见暂停。delta ≥ 1 = 此前的漏检完整区间(先补合成对);
172-
// 旧 own_pending 丢弃(旧期物理未结束——完成增量并入新期下降沿吸收)
173-
out.missed_count = delta as u32;
174-
if delta > 0 {
201+
// 上升沿:可见暂停。先吸收 own_pending 的完成增量(+1——它对应本已
202+
// 可见暂停的物理完成,其恢复沿已按可见边沿发出事件;不吸收会把
203+
// delta 全数当漏检,manual <500ms 往返链上把已见暂停重复合成为
204+
// Pause/Resume 对——DB 序倒挂 + missed_count 误报 + 多余 flush),
205+
// 余下 delta_eff ≥ 1 = 此前的真漏检完整区间(先补合成对);delta 0
206+
// = 旧完成增量未到——待吸收位并入新期(下方 own_pending 保持真,
207+
// 由新期稳态/下降沿吸收一次)
208+
let delta_eff = delta.saturating_sub(u64::from(view.own_pending));
209+
out.missed_count = delta_eff as u32;
210+
if delta_eff > 0 {
175211
push_synth(&mut out, &mut next, interval, reason, session_now);
176212
}
177213
let src = reason.unwrap_or(PauseSource::Manual);

‎app/src-tauri/src/live_session_pause_tests.rs‎

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,22 @@ fn genuine_missed_interval_still_synthesized_after_absorb() {
192192

193193
#[test]
194194
fn flap_during_paused_steady_state_synthesized_at_resume() {
195-
// Arrange:loop 见暂停(处理过),暂停期内用户恢复又暂停(flap)——
196-
// flag 稳态真,seq +1(flap 的 Start 已执行)
195+
// Arrange:loop 见暂停 P1(Pause 事件 @3000,上升沿已处理),随后用户恢复
196+
// +再次暂停都落在同一 <500ms 未采样窗(loop 只见稳态真)——P1 自身区间
197+
// 完成(实测 resume @3500,晚于 Pause 事件时刻 = 恢复沿丢采样)
197198
let v = PauseEdgeView { own_pending: true, pause_processed: true, last_pause_ms: Some(3000), ..view(true, 0) };
198-
// Act:稳态真 delta 1 = 自身完成(absorb 后无漏检)
199+
// Act:稳态真 delta 1 = own 完成(吸收后 delta_eff=0)
199200
let (a1, n1) = plan_edge_observation(&v, true, 1, Some(PauseSource::Manual), Some(iv(3000, 3500, PauseSource::Manual)), 4000);
200-
assert!(a1.events.is_empty(), "自身完成被吸收,不合成");
201-
assert!(!a1.flush_cut);
202-
// Act:第二个 flap 完成(delta 又 +1——真漏检)
201+
// Assert:该次恢复从未落库 → P2-3 补发合成 Resume(实测恢复沿)+ 边界切断
202+
assert_eq!(a1.events.len(), 1);
203+
assert_eq!(a1.events[0].kind, PlanEventKind::Resume);
204+
assert_eq!(a1.events[0].source, PauseSource::Manual);
205+
assert_eq!(a1.events[0].moment_ms, 3500);
206+
assert_eq!(a1.missed_count, 0, "own 完成吸收——非漏检区间,不误报");
207+
assert!(a1.flush_cut, "恢复后采到的内容不得与后续连句——切断边界");
208+
assert!(!n1.own_pending);
209+
assert!(!n1.pause_processed, "再次暂停的上升沿未处理——防御 flush 重新武装");
210+
// Act:暂停期再真漏检一个 flap(另一轮恢复+暂停,delta 又 +1——真漏检)
203211
let (a2, _n2) = plan_edge_observation(
204212
&n1,
205213
true,
@@ -208,9 +216,12 @@ fn flap_during_paused_steady_state_synthesized_at_resume() {
208216
Some(iv(4200, 4400, PauseSource::Media)),
209217
4500,
210218
);
219+
// Assert:真漏检 flap 仍合成一对(合成 Resume 在前,事件时刻保持非降)
211220
assert_eq!(a2.events.len(), 2, "真漏检 flap 合成一对");
221+
assert_eq!(a2.events[0].kind, PlanEventKind::Pause);
212222
assert_eq!(a2.events[0].moment_ms, 4200);
213223
assert!(a2.flush_cut, "flap 内容在引擎流里连句——恢复边沿需断句");
224+
assert!(a2.events[0].moment_ms >= a1.events[0].moment_ms, "DB 序单调");
214225
}
215226

216227
#[test]
@@ -284,3 +295,101 @@ fn init_view_snapshots_paused_state_with_pending_absorption() {
284295
assert!(!v2.flag);
285296
assert!(!v2.own_pending);
286297
}
298+
299+
#[test]
300+
fn rising_absorbs_own_pending_instead_of_duplicate_synth() {
301+
// Arrange(P2-2):上一可见暂停的恢复抢跑(下降沿 delta 0 已发可见
302+
// Resume,own_pending 保持真),其 Start 完成增量直到下一次可见暂停的
303+
// 上升沿才到达(manual <500ms 往返链形态)——delta 1 全数 = own 增量
304+
let v = PauseEdgeView { own_pending: true, pause_processed: true, last_pause_ms: Some(4000), ..view(false, 0) };
305+
// Act:上升沿 + delta 1(槽=own 区间的实测时刻)
306+
let (action, next) = plan_edge_observation(
307+
&v,
308+
true,
309+
1,
310+
Some(PauseSource::Manual),
311+
Some(iv(3000, 4050, PauseSource::Manual)),
312+
6000,
313+
);
314+
// Assert:own 增量被吸收——不得把本已可见的暂停重复合成为 Pause/Resume
315+
// 对(DB 序倒挂 + missed_count 误报 + 多余 flush);只发可见 Pause
316+
assert_eq!(action.events.len(), 1);
317+
assert_eq!(action.events[0].kind, PlanEventKind::Pause);
318+
assert_eq!(action.events[0].moment_ms, 6000);
319+
assert_eq!(action.missed_count, 0, "own 完成增量吸收——不再误报漏检");
320+
assert!(action.flush_cut, "可见暂停上升沿照常断句");
321+
assert_eq!(next.last_pause_ms, Some(6000));
322+
assert!(next.own_pending, "新可见暂停的完成增量待吸收");
323+
assert_eq!(next.seq, 1);
324+
}
325+
326+
#[test]
327+
fn rising_absorbs_own_pending_then_synths_only_extra_missed() {
328+
// Arrange(P2-2 delta 2 语义):own 完成增量与一个真漏检 flap 的完成
329+
// 增量同落一次上升沿观察——吸收 own +1 后余 1 为真漏检(槽只保最近
330+
// 完成区间 = flap,先于可见暂停补合成对)
331+
let v = PauseEdgeView { own_pending: true, pause_processed: true, last_pause_ms: Some(4000), ..view(false, 0) };
332+
// Act
333+
let (action, next) = plan_edge_observation(
334+
&v,
335+
true,
336+
2,
337+
Some(PauseSource::Manual),
338+
Some(iv(5000, 5200, PauseSource::Media)),
339+
9000,
340+
);
341+
// Assert:合成对(真漏检 flap,时刻取槽)+ 可见 Pause;missed_count=1
342+
assert_eq!(action.events.len(), 3);
343+
assert_eq!(action.events[0].kind, PlanEventKind::Pause);
344+
assert_eq!(action.events[0].moment_ms, 5000);
345+
assert_eq!(action.events[0].source, PauseSource::Media);
346+
assert_eq!(action.events[1].kind, PlanEventKind::Resume);
347+
assert_eq!(action.events[1].moment_ms, 5200);
348+
assert_eq!(action.events[2].kind, PlanEventKind::Pause);
349+
assert_eq!(action.events[2].moment_ms, 9000);
350+
assert_eq!(action.missed_count, 1);
351+
assert!(action.flush_cut);
352+
assert!(next.pause_processed);
353+
assert!(next.own_pending);
354+
assert_eq!(next.last_pause_ms, Some(9000));
355+
}
356+
357+
#[test]
358+
fn steady_true_absorb_of_unseen_recovery_synthesizes_resume_and_cuts() {
359+
// Arrange(P2-3 分支):可见暂停的 Pause 事件 @3000 已发出,随后其物理
360+
// 恢复(resume 实测 @3500,晚于 Pause 事件时刻)+再次暂停都落在同一
361+
// <500ms 未采样窗——恢复沿从未被 loop 采到、从未落库
362+
let v = PauseEdgeView { own_pending: true, pause_processed: true, last_pause_ms: Some(3000), ..view(true, 0) };
363+
// Act:稳态真 delta 1 = own 完成(吸收后 delta_eff=0)
364+
let (action, next) = plan_edge_observation(&v, true, 1, Some(PauseSource::Manual), Some(iv(3000, 3500, PauseSource::Media)), 4000);
365+
// Assert:补发合成 Resume(时刻=实测恢复沿,≥ Pause 事件保单调)+ 边界
366+
// 切断(恢复后采到的内容不得与后续连句)+ 防御 flush 重新武装
367+
assert_eq!(action.events.len(), 1);
368+
assert_eq!(action.events[0].kind, PlanEventKind::Resume);
369+
assert_eq!(action.events[0].source, PauseSource::Media);
370+
assert_eq!(action.events[0].moment_ms, 3500);
371+
assert!(action.flush_cut);
372+
assert_eq!(action.missed_count, 0, "own 恢复非漏检——不误报");
373+
assert!(!next.own_pending);
374+
assert!(!next.pause_processed, "再次暂停的上升沿未处理——恢复边沿防御 flush 兜底");
375+
assert_eq!(next.seq, 1);
376+
}
377+
378+
#[test]
379+
fn steady_true_absorb_without_unseen_recovery_stays_silent() {
380+
// Arrange(P2-3 对照——"无未见恢复沿"):own 完成增量是**更早一次已见
381+
// 恢复**(抢跑下降沿已发可见 Resume)的迟到完成:其 Start 在下一可见
382+
// 暂停期间才执行,完成区间 resume(@5000)不晚于已发出的 Pause 事件
383+
// 时刻(@6000 = 下一可见暂停的上升沿)
384+
let v = PauseEdgeView { own_pending: true, pause_processed: true, last_pause_ms: Some(6000), ..view(true, 0) };
385+
// Act:稳态真吸收 own 完成(delta_eff=0)
386+
let (action, next) = plan_edge_observation(&v, true, 1, Some(PauseSource::Manual), Some(iv(4000, 5000, PauseSource::Manual)), 7000);
387+
// Assert:静默吸收——不补 Resume 不 flush(对应恢复已按可见沿发出,
388+
// 补发即重复);防御 flush 状态保持现状
389+
assert!(action.events.is_empty(), "已见恢复的迟到完成增量静默吸收");
390+
assert!(!action.flush_cut);
391+
assert_eq!(action.missed_count, 0);
392+
assert!(!next.own_pending);
393+
assert!(next.pause_processed, "无未见恢复沿——防御 flush 不重新武装");
394+
assert_eq!(next.seq, 1);
395+
}

0 commit comments

Comments
 (0)