fix(runtime-host): serve revision-consistent Usage snapshots - #4068
fix(runtime-host): serve revision-consistent Usage snapshots#4068Sun-GLiang wants to merge 11 commits into
Conversation
…shot-consistency # Conflicts: # packages/runtime-host/src/protocol/index.ts
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for moving Usage reads onto one coherent revision; the Storage/Host/Desktop ownership is much clearer now. I found one remaining lifecycle boundary where a valid reader can lose its revision mid-pagination. This is a suggestion from an outside review, so please feel free to push back if the supported concurrency or latency envelope is intentionally narrower.
AI-assisted review disclosure: Codex ran independent authority and production/test analysis lanes; Astro-Han is the contributor of record for this review.
| retain(contents: UsageSnapshotContents): RetainedUsageSnapshot { | ||
| const now = this.#now(); | ||
| this.#pruneExpired(now); | ||
| while (this.#entries.size >= this.#capacity) { |
There was a problem hiding this comment.
[P2] (category ② — supported concurrency/recovery path)
Thanks for bounding this cache. The bound currently has no notion of an active reader: the fifth overlapping snapshot_start evicts the oldest revision even if that client is still paging it, and the absolute five-minute TTL can likewise expire a valid slow read. This is reachable with normal multi-client/rapid Settings refreshes (and with up to 50,000 log rows over a high-latency remote Host); the Desktop retries the whole snapshot three times, so synchronized readers can keep evicting one another and end in usage_unstable. Could the owner pin revisions per active connection until explicit completion/release, or reject new starts when capacity is occupied rather than evict an in-flight revision, while retaining a hard upper lifetime? A barriered five-reader test plus a slow read across TTL would exercise the boundary. Please push back if there is a production guarantee limiting starts or total pagination below these bounds.
There was a problem hiding this comment.
Codex follow-up: this concern is valid. The previous capacity-4 LRU could evict a revision that an active reader still needed, so the cache now uses connection-owned leases and reserves capacity synchronously before expensive capture begins. A fifth overlapping start returns the typed revision_changed result without entering capture.
Owner reads renew a five-minute idle deadline without extending the 30-minute hard deadline. usage.snapshot.release and connection teardown reclaim both finalized leases and pending reservations; a different connection cannot read or release them. Desktop releases every valid started snapshot from finally, including page-validation and load failures.
The added coverage holds four captures behind a barrier and verifies the fifth is rejected, plus idle/hard expiry, wrong-owner access, explicit release, disconnect cleanup, and Desktop failure cleanup. The branch also merges current main and preserves session-title hydration with a 16-read concurrency bound.
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
…shot-consistency # Conflicts: # apps/desktop/src/main/runtime-host-client.ts # packages/runtime-host/src/protocol/index.ts
…shot-consistency # Conflicts: # docs/windows-test-inventory.md # packages/runtime-host/src/protocol/index.ts # packages/runtime-host/src/server/operation-dispatcher.ts
Astro-Han
left a comment
There was a problem hiding this comment.
The lease rework closes last round's point properly: reserve() has no eviction path, capacity is taken before capture (the barrier test proves the fifth start is refused before any title read), idle renews under a hard cap, ownership is checked on read and release, and release rides the existing releaseConnection seam with a real five-client UDS test for disconnect. Capture is one BEGIN IMMEDIATE on the shared handle, so the consistency claim holds in-process, and paging no longer re-runs the unbounded model-call read per page. Build, 55 host and storage tests, 15 desktop tests, lint, format, typecheck and the architecture check are green locally on bdf0faf5; merge-tree against main is clean.
One thing to fix before merge. Your reply says the fifth start "returns the typed revision_changed result"; the code returns operation_conflict, and loadUsageSnapshot only retries on revision_changed, so the error reaches the Usage page as a hard failure. Two supported paths get there: a single Desktop connection switching ranges quickly holds all four slots until its own finally runs, so a second window or another client fails outright; and a best-effort release that times out on a remote Host leaves the lease until idle expiry, so four of those is five minutes of failure for everyone. Both are recoverable, but #4058 item four asks for a bounded retry of the whole load. Smallest fix: a per-connection cap in reserve() (one is enough for Desktop) and operation_conflict inside the existing MAX_USAGE_SNAPSHOT_ATTEMPTS loop with a short backoff.
Things this PR makes redundant and should take with it: after loadAllLogs goes, usage.query kind: 'logs' has no production consumer, and the Desktop usage:logs and usage:buckets handlers were never exposed by preload on main either. Deleting those two handlers, loadAllBuckets, the logs/buckets protocol variants and the coordinator's usageLogPage family, then folding the old and new page builders, is a few hundred lines of net deletion on an epoch this PR already bumps. Keep kind: 'summary', Session Inspector uses it.
Smaller: the 50,000 activity cap lives in both usage-snapshot-cache.ts and runtime-host-client.ts, and the Desktop copy turns a legitimately larger Host page into invalidProjection; carry it in snapshot_started or the protocol. retain() on the cache is test-only, production goes reserve then finalize. The inner transaction('read') inside the outer write transaction is a pass-through at depth one, and the third acquireOperationalStateDatabase can come from the repos' existing lease. The started.kind !== 'snapshot_started' branch sits outside the try and is unreachable after assertUsageQueryOutputForInput, and the test that pins it can go with it. The body still says epoch 79; the code is 95, which #4386, #4308, #4439, #4500 and #4508 also claim, so re-check at merge.
Evidence boundary: static read of bdf0faf5 against main 92fa5281; runtime-host, storage and desktop usage suites run locally; Playwright not run, and the two-window and rapid-range scenarios are traced, not exercised.
AI-assisted review: drafted with Maka; I verified the capacity error path, the Desktop retry condition and the preload exposure myself.
简体中文
租约改造把上轮的点关干净了:不驱逐、先占容量再捕获、idle 续期加硬上限、归属校验、断连回收走现有接缝,一次 BEGIN IMMEDIATE 保证进程内一致性。本地验证全绿。合并前要修一处:第 5 个 start 实际返回 operation_conflict 而非你回复里说的 revision_changed,Desktop 只对 revision_changed 重试,所以用户看到的是硬失败;单连接快速切 range 就能占满四个 slot 饿死其他客户端。最小修法:reserve() 加每连接上限,并把 operation_conflict 纳入现有重试循环。本 PR 让 usage.query 的 logs 变体和 Desktop 两个从未经 preload 暴露的 IPC handler 变成死代码,建议同 PR 删掉。其余为小项:50,000 上限双权威、test-only 的 retain()、无效的事务嵌套、不可达的 kind 分支、正文 epoch 79 应为 95。
| if (error instanceof UsageSnapshotCapacityError) { | ||
| return { | ||
| ok: false, | ||
| error: { |
There was a problem hiding this comment.
Desktop's loadUsageSnapshot retries only on revision_changed, so this reaches the Usage page as a hard failure. Either retry it there with a short backoff, or cap reservations per connection so one client cannot hold all four slots, ideally both.
| } | ||
|
|
||
| async loadUsageSnapshot(range: TimeRange): Promise<DesktopUsageSnapshot> { | ||
| for (let attempt = 0; attempt < MAX_USAGE_SNAPSHOT_ATTEMPTS; attempt += 1) { |
There was a problem hiding this comment.
#readUsageSnapshot returns undefined only for revision_changed; an operation_conflict from capacity throws straight through this loop. #4058 item four wants the whole load bounded-retried, which this does not yet do.
…shot-consistency # Conflicts: # packages/runtime-host/src/protocol/index.ts
Summary
finallypath, preserve the latestmainsession-title hydration with bounded concurrency, and raise the merged Runtime Host compatibility epoch to 79Fixes #4058
Verification
npm test: all affected tests passed; three unrelated Runtime Host startup-deadline cases timed out under full parallel load and then passed on immediate isolated rerun (25/25)npm run buildnpm run typechecknpm run format:checknpm run lintnpm run windows:inventorynpx knip --workspace apps/desktopnpx knip --workspace packages/uinode scripts/protocol-epoch-check.mjs --base HEAD^1git diff --check upstream/main...HEADAI use
Select exactly one:
Tool(s) and scope: Codex implemented and tested the review remediation, Desktop/Runtime Host lease lifecycle, capacity reservation, and merge-conflict resolution. Sun-GLiang is the human contributor of record.
Checklist
Does this PR entail a change in behavior?