Skip to content

fix(desktop): isolate contexts when Server identity changes - #2291

Merged
hidb4ai merged 10 commits into
oceanbase:masterfrom
jackie-cqz:fix/desktop-deployment-identity
Nov 2, 2026
Merged

hidb4ai merged 10 commits into
oceanbase:masterfrom
jackie-cqz:fix/desktop-deployment-identity

Conversation

@jackie-cqz

@jackie-cqz jackie-cqz commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

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?

  • Bind native read snapshots and immutable write receipts to discovery server_id. Recheck deployment identity after collecting connection facts, before business requests, and before returning Scope/Memory results.
  • Clear the active context and its report on identity loss; invalidate Scope selections, cursors and pending reads. Package version changes alone preserve the deployment identity.
  • Preserve explicit qualified legacy handling only while discovery remains 404. Discovery appearing, disappearing, failing or becoming incompatible requires an explicit connection recheck.
  • Preserve the original write endpoint, Server ID, Principal, Scope and generation. If post-write verification fails, a successful HTTP reply becomes an unknown outcome without returned bodies/references or automatic replay. An old write cannot invalidate a newer active context.
  • Remove an earlier cached verification report before rechecking so failed checks cannot leave stale verification visible.
  • Retain each submitted input in window-local application state before invoking native IPC, independently of the form lifetime. Automatic read disconnection and pre-mutation retry failures preserve the original uncertain input; edited retry input has a separate local identity. Successful receipts clear only their own copy; rejected saves keep their input for explicit recovery/discard, explicit discard clears selected copies, and confirmed navigation clears all copies. Late receipts cannot revive discarded input, clear another attempt or retarget it.
  • Make installed unknown-write acceptance explicitly cancel and confirm navigation discard before starting the next Server scenario, verify no replay, and preserve the separate duplicate-risk confirmation for a subsequent save.
  • Synchronize the current master compatibility declaration without changing upgrade policy, and explicitly select the historical test database in the portable SeekDB publication fixtures.
  • Keep baseline Python acceptance compatible with the maintenance connection's missing-database keyword, wait for real worker completion before consuming descriptor capture, and test both timely and timeout-unknown ZCode Stop outcomes without changing the product's deadline or replay policy.
  • Retry macOS packaging at most three times only when the current attempt reports hdiutil: create failed - Resource busy, waiting 5 and 10 seconds. Preserve combined and per-attempt logs; other build failures remain terminal.
  • Preserve native session observation coverage and reset-window semantics alongside deployment rechecks.
  • Generate the additive nullable MemoryContext.serverId IPC 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:

  • Four regression groups failed against the baseline before the fix. Final native suite: 90 passed, plus a spawned-process proxy-isolation recheck, including fourteen deployment-identity tests covering unchanged Principals, late reads, activation races, legacy transitions, original write targets, uncertain outcomes and preservation of a newer context.
  • 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.
  • Frontend: 148 passed, including the full App native-state refresh/disconnection path in both languages, read-only recovery copy/discard, cleared-form navigation after an uncertain write, retry discovery failure, automatic disconnection while a write is pending, individual discard, late rejected receipts that preserve input, and late successful/unknown receipts after automatic or confirmed context changes. TypeScript, lint/generated drift and production build: passed.
  • Built-wheel SQLite Server acceptance: anonymous loopback, static Bearer, HTTPS with explicit CA/base path and enforced Provider all passed through the native Manager. Includes save/search/exact reads, original Server ID receipts, lost-response writes without replay, Principal changes/revocation and independent Server survival.
  • Server source: 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 in desktop/VALIDATION.md. Production qualification records are unchanged.
  • Deployment replacement, post-write discovery 503, retry rejection and concurrent-read disconnection scenarios use synthetic TCP fixtures. They verify the original pending/unknown receipt and exactly one mutation reaching the fixture; frontend state-refresh regressions use mocked IPC. The new recovery paths have not been rerun locally in installed WebView. Real Server execution does not establish installed Windows 11/macOS acceptance.
  • Python 3.11.15 affected-module checks: 38 passed, 57 skipped (external database probes not enabled). Real worker subprocesses and ZCode/HTTP/SQLite were executed. Node 24.22.1 exercised both cursor-reached and a deliberately delayed timeout-unknown Stop, preserved its pending pause, prevented another Stop from replaying the flush, and verified committed memory/provenance and recall independently. These local results do not establish the Linux/Python matrix outcome.
  • Baseline 176b3f92 synchronization: 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 remains upgrade-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.
  • macOS packaging control-flow validation: extracted the exact workflow Bash step and exercised five scenarios with command substitutes under Git Bash on Windows: immediate success, busy then success, persistent busy, unrelated failure, and busy followed by unrelated failure. Verified attempt limits, exit statuses, waits and all attempt logs. This validates retry control flow, not actual macOS/DMG execution; that remains the new remote job's responsibility.
  • Current baseline 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 system bash instead 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 reproduced SQLITE_BUSY before 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 Windows ty check retains 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.

Copilot AI balanced review requested due to automatic review settings October 28, 2026 08:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copilot AI balanced review requested due to automatic review settings October 29, 2026 05:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI balanced review requested due to automatic review settings October 29, 2026 13:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 knqiufan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread desktop/ui/src/app/MemoryWorkspace.tsx Outdated
)
return;
if (ticket !== lifetime.current || generation.current !== original) return;
onUncertainDraft?.(null);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

knqiufan added a commit that referenced this pull request Oct 30, 2026
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 hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread desktop/ui/src/app/MemoryWorkspace.tsx Outdated
if (isCurrent()) {
setOutcome(result);
if (result.record.status === "unknown") {
onUncertainDraft?.({ text, record: result.record });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@AlexStocks AlexStocks added the 1.5.0 v1.5.0 release tracking label Oct 30, 2026
Copilot AI balanced review requested due to automatic review settings October 30, 2026 09:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment thread desktop/ui/src/app/App.tsx Outdated
Comment on lines +82 to +83
if (record?.status === "succeeded" || record?.status === "failed")
return [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本轮在 head db05cf46 复核,本条及同族两条(4408136537、4420986794)的根因都已闭合:

  1. 提交意图在 IPC 之前就被登记到窗口级状态:MemoryWorkspace.tsx:207-212 在 desktopApi.remember() 之前调用 onSubmission(text, {...}),App.tsx:70-92 的 beginSubmission 把它放进 submittedDrafts(phase pending)。表单卸载不再销毁唯一副本,覆盖 4420986794 的「并发搜索触发 503 断连、表单被 remount」场景。
  2. failed 回执不再删除保留文本:App.tsx:76-91 的 flatMap 只在 record?.status === "succeeded" 时 return [] 清空,其余状态保留该条并把 phase 改成回执状态(failed / unknown),同时把 target 换成回执里的 record.context。这正是本条指出的「503 断连后又返回 403 时唯一文本副本被删掉」的路径。
  3. 恢复卡片在 App.tsx:370-386 独立渲染(SubmittedDraftRecovery),条件只是 draft.target.generation !== active?.generation,不依赖表单是否还在;写后校验失败得到 unknown 时保留的是原 endpoint/serverId/scopeId(4408136537 要求「原始部署可见、不自动重投」)。
  4. 丢弃仍然只走显式路径:恢复卡片的丢弃按钮经 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。

Copilot AI balanced review requested due to automatic review settings October 31, 2026 04:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI balanced review requested due to automatic review settings October 31, 2026 11:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jackie-cqz
jackie-cqz requested review from hireview001 and knqiufan and a balanced review from Copilot October 31, 2026 12:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI balanced review requested due to automatic review settings October 31, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI balanced review requested due to automatic review settings October 31, 2026 13:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hidb4ai
hidb4ai merged commit 745ebf2 into oceanbase:master Nov 2, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.5.0 v1.5.0 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants