Repository navigation
fix(desktop): isolate contexts when Server identity changes - #2291
Conversation
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
| .as_ref() | ||
| .map(|server| server.server_id.as_str()), | ||
| ) | ||
| .await?; |
There was a problem hiding this comment.
[P3] Clear the cached report when the activation fence fails
When a previously checked profile is checked again and discovery changes or fails between observations, this early return leaves its previous entry in inner.reports although the active context was dropped. Clear or replace that report on failure so the subsequent state refresh cannot continue presenting the old deployment and Principal as verified.
There was a problem hiding this comment.
head db05cf46 已按本条处置:session.rs:316 在 check() 进入观测批次之前就执行 inner.reports.remove(id),注释写明 "A failed recheck must not expose a previously verified report"。任何失败出口(deployment_unchanged 返回 Err、观测被取消、generation 已变化)都不会再写回报告;只有成功路径在 session.rs:399 以 inner.reports.insert(id.into(), report.clone()) 重新建立条目,且写回前先校验 inner.generation == generation。
因此「激活栅栏失败后旧部署/Principal 仍显示为已验证」的路径已不存在。同一处置也覆盖 invalidate_profile(:288)与身份失效分支(:505),三者语义一致。
未验证部分:本轮未执行 Rust 侧 cargo test(tests/deployment_identity.rs 新增 772 行),上面的结论来自源码推导,缺少本机行为证据。
hidb4ai
left a comment
There was a problem hiding this comment.
The post-write identity fence introduces a draft-loss regression (inline). The existing activation-report finding also reproduces at this commit.
| .map(|record| record.artifact.clone()) | ||
| .collect(); | ||
| Some(value) | ||
| if let Err(error) = identity_result { |
There was a problem hiding this comment.
[P2] Preserve the submitted draft when post-write verification disconnects
If /v1/memory/remember returns 200 but the subsequent discovery request temporarily returns 503, this new check clears active and increments the generation before returning unknown. NoteForm.save() then refreshes native state, and the [active?.generation] effect clears text and the dirty flag. The user loses the submitted text while being told to inspect an uncertain write; the retained WriteRecord contains no text to recover. I reproduced this through the native Manager and fed its actual state/receipt into MemoryWorkspace: both English and Chinese forms changed preserve me to an empty disabled textarea. The new unknown-write UI test misses this because form() returns the unchanged active state and uses a no-op onState. Keep the original draft available for explicit recovery/discard while clearing private Server results and preventing submission to a different target, and cover the native disconnect/state-refresh path.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
knqiufan
left a comment
There was a problem hiding this comment.
The normal post-write disconnection recovery and cached-report fixes are present. A separate retry path still loses the original uncertain submission; see the inline P2.
| ) | ||
| return; | ||
| if (ticket !== lifetime.current || generation.current !== original) return; | ||
| onUncertainDraft?.(null); |
There was a problem hiding this comment.
[P2] Preserve the uncertain draft when a retry fails before dispatch
After a first save loses its response while identity remains valid, the original text is retained with an unknown receipt. Confirming another save clears that recovery copy here, before the retry is dispatched. If the retry's pre-dispatch discovery check returns 503, the native Manager disconnects and rejects the call without replacing the original unknown receipt. The finally state refresh then remounts the form and clears its text, leaving no copy of the original submission even though the user never confirmed discarding it. I reproduced this using a synthetic TCP fixture's actual Manager states/receipts in the App test; retaining the existing recovery draft makes that test pass. Keep the original uncertain input through a failed retry until it is explicitly discarded or safely resolved, and cover this retry/disconnect path.
There was a problem hiding this comment.
@jackie-cqz Independently rechecked at 4bc80aac1b50816248daf6d217676ac3ecfb839c: this retry failure still reproduces in both English and Chinese. The TCP fixture confirms only the original mutation reached the Server; the failed retry leaves the original unknown receipt intact, but the App loses its submitted text after refreshing the disconnected state. Please preserve that original recovery copy through a pre-dispatch retry failure and add this regression case before requesting approval again.
Integrate #2291 as an explicit dependency and pass the observed Server ID through all five local handoff and retained-Scope identity checks. Preserve retained Scope capture and unconditional stale-report removal. Cover replacement during default/exact reads and retained-Scope refresh.
hidb4ai
left a comment
There was a problem hiding this comment.
The original post-write recovery and cached-report counterexamples now pass. The retry/disconnect finding remains reproducible, and a separate concurrent-read path also loses an in-flight submitted draft (inline).
| if (isCurrent()) { | ||
| setOutcome(result); | ||
| if (result.record.status === "unknown") { | ||
| onUncertainDraft?.({ text, record: result.record }); |
There was a problem hiding this comment.
[P2] Retain in-flight submitted text across automatic disconnection
Recovery is only registered after remember() finishes and while its form is still current. Start a save, run a Memory search while the write is waiting, and let the search's discovery recheck return 503. The failed search refreshes native state with active = null, so App remounts MemoryWorkspace and destroys the submitted text. When the write later returns unknown, isCurrent() is false and recovery is skipped; only the text-free receipt remains. I reproduced this with a paused TCP write and the Manager's actual states in the full App in both languages, without any navigation or discard confirmation. Preserve submitted intent independently of the form lifetime until explicit discard or a conclusive outcome, while retaining the protection against reviving explicitly discarded input.
There was a problem hiding this comment.
@jackie-cqz The PR head is still 4bc80aac1b50816248daf6d217676ac3ecfb839c, so the two previously reproduced draft-loss cases remain unaddressed: a retry rejected before dispatch (#2291 (comment)), and a concurrent read disconnecting the App before the write receipt arrives (this thread). GitHub also now reports CONFLICTING / DIRTY against master at 90355b8e96d68e2932d7980b3e76957f35ea2b44. Please fix both recovery paths, resolve the base conflicts, and push the updated commit with regression results for re-review. No new test run was needed for the unchanged PR snapshot.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hidb4ai
left a comment
There was a problem hiding this comment.
@jackie-cqz The earlier retry and in-flight unknown-write recovery cases now pass at this commit, and the merge conflicts are resolved. A late rejected write can still remove the only recovery copy after automatic disconnection (inline); please preserve the unsaved input in that path.
| if (record?.status === "succeeded" || record?.status === "failed") | ||
| return []; |
There was a problem hiding this comment.
[P2] Keep rejected input after its form disconnects automatically
A failed receipt removes the retained input even when an automatic disconnection has already unmounted the editable form. Hold a save response, let a concurrent search's discovery recheck return 503 (so active becomes null and the recovery copy appears), then return 403/forbidden to the write. The native Manager returns WriteStatus::Failed, and this branch deletes the only remaining text copy although the save was rejected and the user never discarded the input. I reproduced the disappearing recovery text in both languages by feeding the actual TCP fixture/Manager states and receipt into the full App. This violates the documented “Save failures keep the draft” behavior. Keep rejected input available for explicit recovery/discard when no editable draft remains, and cover a late failed receipt after automatic disconnection.
There was a problem hiding this comment.
本轮在 head db05cf46 复核,本条及同族两条(4408136537、4420986794)的根因都已闭合:
- 提交意图在 IPC 之前就被登记到窗口级状态:
MemoryWorkspace.tsx:207-212在desktopApi.remember()之前调用onSubmission(text, {...}),App.tsx:70-92的beginSubmission把它放进submittedDrafts(phasepending)。表单卸载不再销毁唯一副本,覆盖 4420986794 的「并发搜索触发 503 断连、表单被 remount」场景。 failed回执不再删除保留文本:App.tsx:76-91的flatMap只在record?.status === "succeeded"时return []清空,其余状态保留该条并把phase改成回执状态(failed/unknown),同时把target换成回执里的record.context。这正是本条指出的「503 断连后又返回 403 时唯一文本副本被删掉」的路径。- 恢复卡片在
App.tsx:370-386独立渲染(SubmittedDraftRecovery),条件只是draft.target.generation !== active?.generation,不依赖表单是否还在;写后校验失败得到unknown时保留的是原 endpoint/serverId/scopeId(4408136537 要求「原始部署可见、不自动重投」)。 - 丢弃仍然只走显式路径:恢复卡片的丢弃按钮经
confirm()后才onDiscard;confirmSwitch(App.tsx:123-131)在dirty || submittedDrafts.length > 0时先确认再清空;晚到的回执只能命中自己的id(draft.id !== id直接保留),不会复活已丢弃的草稿。
验证:exact head 副本 desktop/ 下 pnpm exec vitest run ui/tests/memory.test.tsx → 52 passed,其中包含 a failed save outcome ... retains its draft、%s unknown save retains its draft and original deployment without replay、late {scope,page}-switch save with {succeeded,unknown} outcome preserves the new draft's navigation guard、confirming an unknown write after the Scope changes cannot submit the old draft、leaving a cleared unknown-write form explicitly discards retained input without clearing the receipt。
未覆盖:desktop/src-tauri/tests/deployment_identity.rs(新增 772 行)本轮未执行 cargo test,「写后 identity 变化 → WriteStatus::Unknown」只有源码推导,缺本机行为证据;该 head 的 tests (3.1x, unit) 四项红与 Recall effort (OceanBase) 已比对为 base 0c1dfc28 同签名失败(run 41081320129 / 41081320353),不属本 PR。
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hidb4ai
left a comment
There was a problem hiding this comment.
Reviewed 81836ad78876726d0f04a3160eb69bb57bb8a90d: the previously reported deployment/draft recovery failures no longer reproduce, including late rejected writes after automatic disconnection. Write targets remain immutable and late receipts update only retained attempts. No new database tables, HTTP endpoints, or IPC commands.
Validation covered the affected native modules, the full frontend suite, all prior counterexamples, owner recovery, and the ZCode/worker cases on Python 3.11. Selected CI checks, including Windows, both macOS architectures, and Linux Desktop, passed.
Limits: the supplemental local Python 3.14 run had process/startup timeouts and a file-log assertion failure; Python 3.11 reruns passed. Installed-package CI was skipped, so installed qualification remains unverified here.
Which issue or RFC does this PR close?
Refs #2101 (deployment identity portion of PR 01-B) and #2103, following RFC #1455. Both issues remain open for their remaining delivery and acceptance work.
Rationale for this change
Replacing a Server behind the same endpoint can preserve the same Principal while changing the deployment. Desktop previously rechecked only the Principal, allowing old Scope selections and late private results to cross that replacement.
What changes are included in this PR?
server_id. Recheck deployment identity after collecting connection facts, before business requests, and before returning Scope/Memory results.testdatabase in the portable SeekDB publication fixtures.hdiutil: create failed - Resource busy, waiting 5 and 10 seconds. Preserve combined and per-attempt logs; other build failures remain terminal.MemoryContext.serverIdIPC field, display the original deployment in write recovery feedback, and document behavior and evidence in both README locales and the validation/security guides.Are there any user-facing changes?
An observed deployment or Principal change disconnects the private context and requires rechecking the connection and selecting the Scope again. Unknown-write feedback identifies the original deployment and preserves the submitted input for manual recovery in the current window. Closing the window loses this recovery draft. No HTTP API, credential or persisted-profile format changes.
Identity is observed around requests. The public mutation contract has no conditional Server-identity precondition, so this cannot atomically prevent replacement between verification and dispatch or detect replacement that changes back between observations. Qualified legacy Servers have no durable deployment fence. Explicit readiness separation and local setup handoff remain follow-up work; existing anonymous readiness checks remain.
How was this change tested?
Windows 10 x64, Python 3.11.15 and Rust 1.95.0:
cargo +1.95.0 test --locked --manifest-path desktop/src-tauri/Cargo.toml; Clippy with--all-targets -- -D warnings; Rust formatting and IPC export drift: passed.aedf7beb1ecff2ea1fade6ccdd8e4bf6a298f799; wheel SHA-256:fde108a81a872a6228aad54832f2cd8f899a53bb4a7ff5c321871891fdbc5b68. This used the disposable fixture qualification with a dirty Desktop tree; executable/contract hashes and measured local round trips are recorded indesktop/VALIDATION.md. Production qualification records are unchanged.176b3f92synchronization: 109 passed, 6 Windows skips across the three portable mocked-PyMySQL publication regressions and upgrade-gate tests; 61 integration manifest tests passed. The three publication cases were run independently of their module's POSIX indexing guard; this does not qualify native indexing on Windows. The upgrade policy remainsupgrade-required; no waiver or digest override was added. Root/secondary lock checks, release/API/JS generation checks, integration guidance/docs, action pins, full Desktop checks and both Linux-target type checks passed with locked dependencies. Earlier migration/worker/ZCode targeted checks also passed on this baseline. These local checks do not establish external database acceptance or the remote Linux matrix result.0a98473a: synchronized the consolidated Main/reusable CI workflows and preserved the MySQL maintenance-admission fixture contract. CI/upgrade/migration/owner-recovery checks with two pytest workers: 254 passed, 10 skipped; two additional upstream Shell cases resolve Windows systembashinstead of Git Bash and fail locally. All 8 upgrade-gate input combinations passed when executing the exact checked-in script through explicit Git Bash. Owner recovery separately: 16 passed, with all concurrent replay, original-relation, audit uniqueness and rollback assertions retained. Its disposable SQLite fixtures use a 30-second busy budget; the production five-second default is unchanged. A controlled real-SQLite six-second write lock reproducedSQLITE_BUSYbefore that fixture adjustment and passed afterward; this establishes contention handling locally, not the precise duration of the remote runner's lock. Linux-target type checks, upgrade policy, 225 action references and locked dependencies passed.uv run --no-sync prek run -a: all file hooks passed; default Windowsty checkretains existing POSIX-only attribute diagnostics in unchanged Python files.ty check --python-platform linux: passed.AI usage statement
OpenAI Codex (GPT-6) assisted with implementation, local review, regression reproduction, testing and documentation. Commands and real-Server harnesses were executed locally; qualification limits are recorded above.