Skip to content

Persist offline environment metadata#3795

Open
juliusmarminge wants to merge 3 commits into
mainfrom
agent/offline-environment-cache
Open

Persist offline environment metadata#3795
juliusmarminge wants to merge 3 commits into
mainfrom
agent/offline-environment-cache

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jul 8, 2026

Copy link
Copy Markdown
Member

What changed

  • Persist the last complete server configuration, including provider/model metadata.
  • Persist unfiltered workspace Git refs for offline branch selection.
  • Hydrate cached values while a connection is unavailable, then replace them after a successful reconnect.

Why

Project and thread shells were cached, but model and branch pickers only read live server responses. As a result, an offline device could not configure a pending task.

Impact

Mobile users can select their last-known model and branch while offline to create pending tasks. The same durable cache is available in web clients, and cached data is cleared with its environment.

Validation

  • vp check
  • vp run typecheck
  • vp run lint:mobile
  • vp test packages/client-runtime/src/state/server.test.ts packages/client-runtime/src/state/vcs.test.ts

Note

Medium Risk
Touches shared persistence and atom wiring across web/mobile with an IndexedDB version bump; wrong cache eligibility for VCS refs could mislead branch pickers, but scope is limited to offline UX rather than auth or server data.

Overview
Offline task creation can use the last-known server configuration (including provider/model metadata) and unfiltered Git branch lists when the environment is disconnected.

EnvironmentCacheStore gains loadServerConfig / saveServerConfig and loadVcsRefs / saveVcsRefs, implemented on web (IndexedDB v4, new object stores) and mobile (file-backed caches). Environment clear removes these entries alongside existing shell/thread caches. Mobile and catalog paths also standardize on Schema.fromJsonString encode/decode instead of manual JSON.parse / stringify.

Server config atoms no longer subscribe only over the socket: makeEnvironmentServerConfigState hydrates from cache, follows subscribeServerConfig, debounces persistence (~500ms), and flushes on scope end. VCS listRefs atoms use makeCachedVcsRefsState—cache only for the default unfiltered list (limit 100); filtered/paginated queries stay live-only—and refresh when the connection becomes available.

Reviewed by Cursor Bugbot for commit 018c39f. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Persist server config and VCS refs offline in mobile and web storage

  • Adds loadServerConfig/saveServerConfig and loadVcsRefs/saveVcsRefs to the EnvironmentCacheStore interface in persistence.ts, with implementations for mobile (file-based) and web (IndexedDB).
  • Server config state in state/server.ts now loads from cache on startup, debounces and persists live updates, and surfaces cached values to consumers before a live connection is established.
  • VCS refs state in state/vcs.ts loads cached refs when eligible (unfiltered, up to 100 results), refreshes on reconnect, and writes results back to the cache.
  • Web storage bumps DATABASE_VERSION from 2 to 4, triggering an IndexedDB upgrade to provision the new SERVER_CONFIG_STORE_NAME and VCS_REFS_STORE_NAME object stores.
  • EnvironmentCacheStore.clear now also removes cached server config and VCS refs in both platforms.
  • Risk: the IndexedDB version bump will trigger an upgrade migration for all existing web users.

Macroscope summarized 018c39f.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b0c2085f-9332-4cc3-a374-1c33aeb04bf3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/offline-environment-cache

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 8, 2026
@juliusmarminge juliusmarminge marked this pull request as ready for review July 8, 2026 11:39
@macroscopeapp

macroscopeapp Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces new offline caching capabilities for server config and VCS refs. Three unresolved review comments identify potential bugs: race conditions in VCS refresh, error handling leaving UI pending indefinitely, and no recovery path for corrupt cache files. These substantive issues warrant human review.

You can customize Macroscope's approvability policy. Learn more.

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit cec0ff7. Configure here.

onNone: () => Result.failVoid,
onSome: (value) => Result.succeed(value),
}),
),

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.

VCS failures leave pending state

Medium Severity

When a live vcsListRefs refresh fails, the error is logged but not propagated. The SubscriptionRef remains Option.none, and the cachedVcsRefsChanges stream filters out these empty options. This leaves useEnvironmentQuery pending indefinitely without surfacing the RPC failure, unlike the previous query atom.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cec0ff7. Configure here.

),
),
Effect.forkScoped,
);

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.

Overlapping VCS refresh races

Medium Severity

Each connection-generation change triggers refresh() without serialization. If a new reconnect fires before an earlier vcsListRefs request finishes, responses can land out of order and an older result may overwrite newer branch data in memory and in the durable cache.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cec0ff7. Configure here.

Comment on lines +436 to +441
const stored = yield* decodeStoredServerConfig(raw).pipe(
Effect.mapError((cause) => shellPersistenceError("load-server-config", cause)),
);
return stored.environmentId === environmentId
? Option.some(stored.config)
: Option.none();

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.

🟡 Medium connection/storage.ts:436

loadServerConfig propagates a decode failure from a corrupt cache file without deleting the file, so the unreadable file permanently blocks offline server-config hydration on every subsequent launch. In contrast, loadShell and loadThread both decode the file content but also have a fallback path (legacy file or returning Option.none()), whereas a corrupt server-config cache has no such recovery — the error always surfaces and the file is never removed. Consider deleting the file on decode failure and returning Option.none(), matching the catalog corruption-handling pattern in catalog-store.ts.

          const stored = yield* decodeStoredServerConfig(raw).pipe(
-            Effect.mapError((cause) => shellPersistenceError("load-server-config", cause)),
+            Effect.catchAll((cause) =>
+              Effect.tryPromise({
+                try: () => file.delete(),
+                catch: () => {},
+              }).pipe(
+                Effect.asOptionNone as Effect.Effect<Option.Option<never>, never, never>,
+                Effect.as(Option.none() as Option.Option<ServerConfig>),
+                Effect.catchAll(() => Effect.succeed(Option.none() as Option.Option<ServerConfig>)),
+              ),
+            ),
          );
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/connection/storage.ts around lines 436-441:

`loadServerConfig` propagates a decode failure from a corrupt cache file without deleting the file, so the unreadable file permanently blocks offline server-config hydration on every subsequent launch. In contrast, `loadShell` and `loadThread` both decode the file content but also have a fallback path (legacy file or returning `Option.none()`), whereas a corrupt server-config cache has no such recovery — the error always surfaces and the file is never removed. Consider deleting the file on decode failure and returning `Option.none()`, matching the catalog corruption-handling pattern in `catalog-store.ts`.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant