Skip to content

feat(desktop): add runtime store#464

Open
cline-cloud[bot] wants to merge 1 commit into
mainfrom
cline/split-440-runtime-store
Open

feat(desktop): add runtime store#464
cline-cloud[bot] wants to merge 1 commit into
mainfrom
cline/split-440-runtime-store

Conversation

@cline-cloud
Copy link
Copy Markdown

@cline-cloud cline-cloud Bot commented May 8, 2026

Split from #440.

This PR adds the packaged desktop runtime store primitives only:

  • on-disk runtime-store layout under userData
  • strict current.json pointer validation and atomic writes
  • bad-version tracking and partial/version cleanup helpers
  • unit coverage for pointer validation, canonical-path enforcement, bad versions, and partial cleanup

Kept as the first slice so follow-up updater/orchestrator PRs can depend on these helpers.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR introduces the on-disk runtime-store primitives for the packaged desktop runtime: a current.json pointer with strict canonical-path validation, a bad-versions.json registry, and partial/version cleanup helpers, along with comprehensive unit tests. It is the foundational slice for subsequent updater/orchestrator PRs.

  • runtime-store.ts: New module covering pointer read/write with atomic renames, path-traversal guards, semver validation throughout, and best-effort partial cleanup.
  • test/runtime-store.test.ts: Full unit coverage of pointer round-trips, canonical-path enforcement, bad-version CRUD, and path-traversal edge cases.
  • package.json / package-lock.json: semver ^7.6.3 added as a runtime dependency (correctly placed in dependencies, not devDependencies); lockfile deduplicated so semver 7.7.4 is hoisted to top-level.

Confidence Score: 5/5

Safe to merge; the new runtime-store module is well-structured with thorough validation and tests, and no new blocking defects were found.

All changed code is additive (new module + tests), the canonical-path enforcement in readPointer/writePointer is solid, and test coverage is broad. The one new observation — isBadVersion silently returning false for invalid input — is non-blocking since callers in this slice always validate versions before calling it.

packages/desktop/src/runtime-store.ts — atomicWrite, cleanupPartials, and markBadVersion have edge-case gaps flagged in open review threads.

Important Files Changed

Filename Overview
packages/desktop/src/runtime-store.ts New module implementing the on-disk runtime pointer, bad-version registry, and partial-cleanup helpers; well-structured with strict path validation, but atomicWrite has an orphaned-temp risk and cleanupPartials has a TOCTOU gap flagged in review threads.
packages/desktop/test/runtime-store.test.ts Comprehensive unit tests covering pointer round-trips, canonical-path enforcement, bad-version CRUD, and partial/version cleanup; good coverage of path-traversal edge cases.
packages/desktop/package.json Added semver ^7.6.3 as a runtime dependency and @types/semver ^7.5.8 as a devDependency; placement is correct.
packages/desktop/package-lock.json Lockfile updated to hoist semver 7.7.4 to top-level (satisfies ^7.6.3) and scope the former top-level semver 6.3.1 under @electron/get; deduplication is correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Boot / Updater] -->|readPointer| B{current.json\nexists & valid?}
    B -- No --> C[Use bundled runtime]
    B -- Yes --> D{cliEntry file\nexists on disk?}
    D -- No --> C
    D -- Yes --> E{isBadVersion?}
    E -- Yes --> C
    E -- No --> F[Spawn staged runtime]
    F -- crash --> G[markBadVersion\n+ clearPointer]
    G --> C

    H[Updater / Stager] -->|writePointer| I[atomicWrite\ntmp to rename\ncurrent.json]
    H -->|stage new version| J[Extract to\nversions/v.partial/]
    J --> K[rename to\nversions/v/]
    K --> I

    L[Startup] -->|cleanupPartials| M[Remove *.partial dirs\nbest-effort]
    L -->|removeVersionDir| N[rm versions/v/]
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/desktop/src/runtime-store.ts:205-207
The `isBadVersion` function silently returns `false` when given a non-semver `version` string rather than propagating an error. This means a caller that accidentally passes an invalid version string (e.g., a build tag like `"nightly-20250507"`) would receive `false` — indistinguishable from "this version is known good" — and could proceed to stage or launch a version that was never actually checked. Throwing (or at minimum returning a distinct sentinel) would surface the mistake at the call site rather than silently allowing it.

```suggestion
export function isBadVersion(userData: string, version: string): boolean {
	if (!isSemver(version)) {
		throw new Error(`runtime-store: invalid semver: ${version}`);
	}
	return readBadVersions(userData).includes(version);
}
```

Reviews (2): Last reviewed commit: "feat(desktop): add runtime store" | Re-trigger Greptile

Comment on lines +172 to +180
export function cleanupPartials(userData: string): void {
const versions = path.join(root(userData), "versions");
if (!existsSync(versions)) return;
for (const e of readdirSync(versions, { withFileTypes: true })) {
if (e.isDirectory() && e.name.endsWith(".partial")) {
rmSync(path.join(versions, e.name), { recursive: true, force: true });
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 cleanupPartials can throw despite "best-effort" label

The existsSync guard and the readdirSync call are not atomic. If the versions directory is removed by an external agent (system cleanup, antivirus, parallel test run) between the two calls, readdirSync throws an uncaught ENOENT that propagates to the caller — exactly the failure mode that "Best-effort" is supposed to suppress. A try-catch around the loop body (or the whole function) would match the documented intent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/desktop/src/runtime-store.ts
Line: 172-180

Comment:
**`cleanupPartials` can throw despite "best-effort" label**

The `existsSync` guard and the `readdirSync` call are not atomic. If the `versions` directory is removed by an external agent (system cleanup, antivirus, parallel test run) between the two calls, `readdirSync` throws an uncaught `ENOENT` that propagates to the caller — exactly the failure mode that "Best-effort" is supposed to suppress. A try-catch around the loop body (or the whole function) would match the documented intent.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +86 to +91
function atomicWrite(target: string, body: string): void {
mkdirSync(path.dirname(target), { recursive: true });
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
writeFileSync(tmp, body);
renameSync(tmp, target);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Orphaned .tmp file if renameSync throws

writeFileSync succeeds, writing a full temp file to disk, and then if renameSync fails (e.g. Windows antivirus lock, EXDEV on a bind-mount), the temp file is never cleaned up. Accumulated .tmp files live next to current.json and bad-versions.json indefinitely. A try/catch on renameSync that deletes the temp file before re-throwing would keep the store tidy.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/desktop/src/runtime-store.ts
Line: 86-91

Comment:
**Orphaned `.tmp` file if `renameSync` throws**

`writeFileSync` succeeds, writing a full temp file to disk, and then if `renameSync` fails (e.g. Windows antivirus lock, EXDEV on a bind-mount), the temp file is never cleaned up. Accumulated `.tmp` files live next to `current.json` and `bad-versions.json` indefinitely. A `try/catch` on `renameSync` that deletes the temp file before re-throwing would keep the store tidy.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +209 to +219
export function markBadVersion(userData: string, version: string): void {
if (!isSemver(version)) {
throw new Error(`runtime-store: invalid semver: ${version}`);
}
const set = new Set(readBadVersions(userData));
set.add(version);
atomicWrite(
badVersionsPath(userData),
`${JSON.stringify([...set].sort(semver.compare))}\n`,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Read-modify-write in markBadVersion is not application-level atomic

atomicWrite makes the final rename atomic at the filesystem level, but the read-then-write cycle is not. If two callers mark different versions bad concurrently (e.g. a crash-reporter and a background worker both detect a failing version), the second writer's Set is seeded only from its own read — it never sees the first writer's addition — so renameSync silently overwrites and loses the first entry. The existing comment for bad-version pruning acknowledges this area is informal, but the lost-update window is worth calling out explicitly since the comment doesn't mention it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/desktop/src/runtime-store.ts
Line: 209-219

Comment:
**Read-modify-write in `markBadVersion` is not application-level atomic**

`atomicWrite` makes the final rename atomic at the filesystem level, but the read-then-write cycle is not. If two callers mark different versions bad concurrently (e.g. a crash-reporter and a background worker both detect a failing version), the second writer's `Set` is seeded only from its own read — it never sees the first writer's addition — so `renameSync` silently overwrites and loses the first entry. The existing comment for bad-version pruning acknowledges this area is informal, but the lost-update window is worth calling out explicitly since the comment doesn't mention it.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant