fix(clipboard): attribute multi-event OS echoes per write instead of one-shot - #51
fix(clipboard): attribute multi-event OS echoes per write instead of one-shot#51moekyo wants to merge 8 commits into
Conversation
…one-shot A single programmatic clipboard write can surface as several watcher events (OS re-encode, double change notification, clipboard-manager re-assertion). The one-shot next-change fallback absorbed only the first; the second was mis-attributed as LocalCapture and dispatched back to the sender, sustaining an A<->B image echo loop. The fallback now carries an echo budget per write (two credits for remote pushes, one for local restores), a content match spends one credit of the paired fallback instead of deleting it, fallback consumption is scoped to the same content kind (image/text/files/ rich-text), and the leftover credit expires ECHO_TAIL_TTL after the first echo so an unrelated later copy is not swallowed. Local restore semantics are unchanged (one-shot).
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
真机验证记录(macOS + Windows 双端)
|
|
@mkdir700 麻烦 review 一下这个修复。问题:两台已配对设备只截一张图 → 本机面板同一张图不断新增、对端不断收到同一张图(捕获/同步风暴)。根因:SelfWriteLedger 一次性归因无法吸收一次写入产生的多个 OS 事件,第二个事件被判 LocalCapture 回传对端形成 A↔B 回环。已附单元测试、跨层探针和 macOS+Windows 真机验证记录。 |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…g recovers A crash mid-admission (e.g. Windows blue screen during a join) leaves a durable 'admission in progress' record in the admission store. The last-resort /encryption/factory-reset path wiped key material and peer state but never cleared that record, so every later pairing attempt failed on both sides ( / with AdmissionInProgress) and the user was locked out of re-pairing. factory_reset now clears the admission store through the same clear_admission_state entry used by the device-management reset; a failed admission-store reset surfaces as AdmissionStateResetFailed instead of being swallowed.
|
补充第二个修复(同分支):factory reset(忘记口令兜底路径)现在会清理持久化的 admission 状态 —— 崩溃/蓝屏中断加入流程留下的 "admission in progress" 记录此前会导致重置后仍无法重新配对,现已修复。 |
A crash or interruption during pairing leaves a durable pending admission attempt that blocks every later pairing attempt. Add a lightweight recovery operation that clears only the non-terminal attempts - terminals, membership history, consumed invitations and the device trust revision of an intact space are preserved. - admission_attempt_store: clear_pending_admissions (attempts.retain non-terminal only) - port: AdmissionAttemptRepositoryPort::clear_pending_admissions - convergence: WorkspaceConvergence::clear_pending_admissions + ProfileWorkspaceConvergence::clear_stale_admission - engine: Operation::ClearStaleAdmission -> OperationResult::StaleAdmissionCleared
… swallowing Windows joiner fails at 'prepare Candidate: space transition storage failed' because init_db_pool (SqliteConnection::establish + WAL pragma) errors are collapsed to the unit AdmissionSpaceTransitionError::Storage, hiding the real diesel/libsqlite error. Log the underlying error plus the target database path and space id before collapsing; same for the manifest-store load in prepare_if_needed.
The 'space transition storage failed' on Windows joiner is collapsed at three unlogged points, hiding the real sqlite/diesel error: - prepare_if_needed now logs the branch decision (source space vs target space, manifest presence) - prepare_source failures now log the underlying String error plus generation/attempt ids - snapshot_to_bytes logs the diesel error for VACUUM INTO (e.g. 'cannot VACUUM from within a transaction', 'database is locked') with the scratch path and SQL
…torage
The Windows joiner failure ('space transition storage failed') was
invisible because every underlying sqlite/diesel/fs error was collapsed
into a unit Storage variant and the diagnostic tracing line could be
dropped by the lossy non-blocking writer. Make the error itself carry
the detail: Storage is now Storage(String) via a storage() constructor,
so the real failure text survives map_space_transition_error ->
WorkspaceConvergenceError::AdmissionStorage and appears in the
join-space error line and UI regardless of log writer behavior.
Windows joiner failed at 'space transition storage failed: open generation parent': std::fs::File::open on a directory requires FILE_FLAG_BACKUP_SEMANTICS, which the standard API does not pass, so opening the generation directory for fsync always fails with access denied on Windows (macOS/Linux allow read-only directory opens). Use the same pattern as active_space_manifest_store::sync_parent_directory: Unix opens+syncs the directory; Windows skips it (renames are already durable via MOVEFILE_WRITE_THROUGH).
|
Windows joiner 修复验证通过(7efa66ec) 问题:Windows 作为 joiner 加入空间必败 根因: 修复:复用 验证:Windows 实机以 joiner 身份用邀请码加入 → 成功打通(此前 5 个版本必败)。 |
|
感谢你的 PR,这两天我在重新梳理 application 层(AI 写的太乱了),不可避免会有冲突,所以 PR 暂时无法合并 |
我自己先打包用着了,不急的 :) |
问题类型
结构性缺陷(origin 归因语义:一次性消费 → 按单次写入的回显集)。
根因
一次程序化剪贴板写入可能产生多个 watcher 事件(OS re-encode、双变更通知、剪贴板管理器重断言)。原
InMemorySelfWriteLedger的 next-change fallback 只吸收第一个事件,第二个事件无 guard 命中 → 被判为LocalCapture→dispatch_local_capture回传原设备 → 对端 apply 后再写 OS 剪贴板 → 形成 A↔B 图片回环。跨层身份不一致:watcher 用解码像素指纹去重,ledger 用原始字节 hash,re-encode 后 ByContent 永不命中,完全依赖这个一次性 fallback。用户现象:两台已配对设备,只截一张图 → 本机面板同一张图不断新增、对端不断收到同一张图(捕获/同步风暴)。
方案
image:/text:/files:/rich-text:前缀)匹配,无关类别的事件不再抢占。ECHO_TAIL_TTL(5s),后续无关复制不被吞。验证
cargo test -p uc-infra --lib clipboard::change_origin:12 通过(含新增回环回归用例)。cargo check --workspace --all-targets --locked(585 crates)、cargo fmt --all -- --check、node scripts/architecture/check-engine-repository.mjs通过。uc-infra全量测试中rendezvous::client::tests::transport_failure_maps_transport_variant在本机失败,已在基础提交(31c149c)上复现,与本改动无关(环境相关)。文档
已按仓库规范在
docs/architecture/architecture-bible.md文档维护记录中登记本次语义变化。