Skip to main content

Update System

Automatic detection and installation of bundle updates.

Components

ComponentResponsibility
UpdateSchedulerTiming of checks (startup, daily/weekly)
UpdateCheckerCompare installed vs latest versions
AutoUpdateServiceBackground updates with rollback
UpdateCacheCache results with configurable TTL

Configuration

SettingDefaultDescription
promptregistry.updateCheck.enabledtrueEnable update checks
promptregistry.updateCheck.frequencydailydaily, weekly, manual
promptregistry.updateCheck.autoUpdatefalseGlobal gate for auto-updates
promptregistry.updateCheck.cacheTTL300000Cache TTL (5 min)
promptregistry.updateCheck.notificationPreferenceallall, major, none

Update Check Flow

flowchart TD
A[Extension activates] --> B[Wait 5 seconds]
B --> C[UpdateScheduler → UpdateChecker]
C --> D[Check cache UpdateCache]
D --> E{Cache status}
E -->|valid| F[Return cached]
E -->|expired| G[RegistryManager.checkUpdates]
G --> H[Compare installed vs latest]
H --> I[Cache results]
I --> J[Show notification if updates available]

Auto-Update Logic

Hybrid approach: Global autoUpdate setting + per-bundle opt-in

For auto-update to occur:

  1. Global updateCheck.autoUpdate = true
  2. Per-bundle auto-update enabled (via "Enable Auto-Update" command)

Concurrency Control

  • Batch Size: 3 concurrent updates
  • Active Updates Set: Prevents duplicate operations
  • Check-in-Progress Flag: Prevents overlapping cycles

Dependency Injection

Uses interfaces to avoid circular dependencies:

interface BundleOperations {
updateBundle(bundleId: string, version?: string): Promise<void>;
listInstalledBundles(): Promise<InstalledBundle[]>;
}

class AutoUpdateService {
constructor(
private readonly bundleOps: BundleOperations,
private readonly sourceOps: SourceOperations
) {}
}

Hub Sync Scheduler

Separate from bundle update checks, HubSyncScheduler keeps the active hub configuration fresh:

ComponentResponsibility
HubSyncScheduler24h periodic timer calling HubManager.syncActiveHub()
HubManager.onHubSyncedEvent fired after every hub sync (startup, manual, periodic)

Source sync is event-driven: a single onHubSynced listener in extension.ts triggers syncAllSources({ silent: true }) after any hub sync, ensuring sources stay in sync regardless of how the hub sync was initiated.

Source Registration and Pruning

loadHubSources (in packages/app) reconciles a hub config's sources[] declarations with the registry on every sync. A stored source's id is derived from its location — generateSourceId(type, url, { branch, collectionsPath }) — so the same URL declared by two hubs resolves to one stored source, and changing a declaration's url produces a different stored id.

Each sync tracks two distinct sets:

SetContentsPurpose
Protected from pruningEvery declared source id, including disabled declarations and matched duplicatesSources in this set are never removed
Registered this cycleOnly ids a successful addSource or updateSource wrote during this syncThe only pool eligible as a remap target

A stored source belonging to the synced hub whose id is not protected is an orphan — typically the pre-rename record left behind by a url change.

Matching an Orphan to Its Replacement

Each stored hub source carries hubSourceId, the author-assigned sources[].id from the hub config. That value is stable across a url change, so it is the matching key rather than the URL-derived id.

An orphan's replacement candidates are the sources registered this cycle whose (hubId, hubSourceId) pair equals the orphan's. Since hubSourceId is unique within a hub by schema, the pair is unique across hubs.

  • Exactly one candidateremapBundleSource moves installed-bundle references (repository-scope lockfile, plus user and workspace scope) from the orphan id to the candidate id, then the orphan is removed. The remap resolves the replacement descriptor before any write, so a missing replacement fails without mutating anything.
  • Anything else — keep-alive.

Requiring exactly one candidate is what makes selection deterministic: a one-element result is independent of the order concurrent workers populated the map in, so concurrency: 4 and concurrency: 1 pick the same id.

Keep-Alive

Keep-alive leaves the orphan in storage, leaves its installed-bundle records untouched, and emits one warn naming the orphan id and name, the number of referencing installed bundles, the reason, and the remediation. It applies when:

  • zero candidates match,
  • two or more candidates match,
  • the orphan carries no hubSourceId,
  • the remap failed,
  • or the remapBundleSource port is not wired.

A guessed remap silently detaches bundles from their real origin and is unrecoverable; a kept orphan is inert and resolvable on the next sync. Two further guards: an orphan with zero referencing installed bundles is removed outright without a remap, and if any addSource failed during the sync, all orphan evaluation, removal, and remapping is skipped for that cycle.

Sticker Backfill

hubSourceId is optional, so sources persisted before the field existed have none, and orphan handling for them falls back to keep-alive. Every successful sync writes hubSourceId on both added and updated sources, so the next successful sync of the owning hub backfills the field and restores deterministic matching.

See Also