Skip to main content

Extension Lifecycle

Overview

Every extension in the code editor follows a well-defined lifecycle that governs how it is discovered, loaded, executed, and shut down. The lifecycle is managed by two layers: the host orchestrates activation, crash recovery, and resource cleanup, while the JS runtime handles code execution and communication.

Understanding this lifecycle is essential for extension authors who need reliable initialization, predictable teardown, and awareness of crash recovery behavior.

Lifecycle Phases

An extension moves through six distinct phases from installation to shutdown:

                           manifest.json parsed
contributions registered in registry
REGISTRATION ──────────────────────────────────────────────────────>

│ JS runtime created in isolated environment
│ extension code evaluated, module system injected
ACTIVATION ────────────────────────────────────────────────────────>

│ __triggerOnLoad() called in JS
│ extension performs async initialization (30s timeout)
ON LOAD ───────────────────────────────────────────────────────────>

│ extension handles commands, responds to events,
│ makes RPC calls, interacts with the editor
RUNNING ───────────────────────────────────────────────────────────>

│ __triggerOnDispose() called in JS
│ extension cleans up its own resources
ON DISPOSE ────────────────────────────────────────────────────────>

│ worker disposed, isolate killed,
│ host-side resources cleaned up
DEACTIVATION ──────────────────────────────────────────────────────>

Phase 1 -- Registration

When the app starts (or an extension is installed), the host reads the extension's manifest.json and parses it. The manifest is registered, and its contribution points (commands, themes, settings, etc.) are made available to the editor.

Registration happens regardless of whether the extension has runtime code (main field). Extensions that only contribute static assets (e.g., a theme pack) stop here and never enter later phases.

The extension ID is validated during registration: it must contain only alphanumeric characters, hyphens, and underscores, and be at most 128 characters long.

Phase 2 -- Activation

For extensions with a main field, the host creates a JavaScript runtime and loads the extension code. The activation strategy depends on the extension's activationEvents:

Activation EventBehavior
onStartupFinishedActivated immediately during app bootstrap
*Activated immediately (wildcard, same as above)
onCommand:<commandId>Deferred until the command is first invoked (lazy activation)
onFileOpen:<glob>Deferred until a matching file is opened (lazy activation)

Engine version check. Before activating, the host compares the extension's engineVersion from the manifest against the app's current engine version (currently 0.1.0). The version is parsed as semver — the major version must match exactly, and within the same major the current engine must be >= the required version. If the check fails, the extension is skipped.

Lazy activation. Extensions with onCommand: or onFileOpen: events are registered for deferred activation. For onCommand: events, placeholder commands are registered in the command palette that trigger activation on first invocation. For onFileOpen: events, the host checks each file open against the glob pattern and activates matching extensions. Once the runtime is initialized, it signals readiness and the deferred command is forwarded immediately.

Phase 3 -- onLoad

Once the JS runtime is created and the extension code is evaluated, the host calls __triggerOnLoad() in the JS context. This is the extension's opportunity to:

  • Register event listeners
  • Set up internal state
  • Make initial RPC calls to the editor (e.g., reading configuration)
  • Register additional commands dynamically

The onLoad phase has a 30-second timeout. If the extension's onLoad callback does not complete within this window, the host logs a warning and continues -- it stops waiting and moves on. The extension remains active, but any incomplete async work in onLoad is abandoned. Note that the onLoad function is not killed mid-execution; the host simply stops waiting for it to resolve.

Phase 4 -- Running

The extension is now fully active and can:

  • Handle commands -- The host calls __executeCommand() in JS when a registered command is invoked
  • Respond to events -- The host calls __pushEvent() for editor events (file open/close/save, tab changes, settings changes, file creation/deletion/rename/move)
  • Make RPC calls -- The extension calls sendMessage('rpc', ...) to invoke editor APIs (up to 50 concurrent RPC calls per extension)
  • Receive WebSocket events -- The host calls __pushWsEvent() for WebSocket data

Phase 5 -- onDispose

Before shutdown, the host calls __triggerOnDispose() in the JS context. This gives the extension a chance to clean up resources such as:

  • Canceling timers and intervals
  • Closing connections
  • Persisting state

The host calls this callback before shutting down the JS runtime, giving the extension a brief window to save state and release resources.

warning

onDispose is called when the extension is disabled or uninstalled through the app. However, it is not guaranteed in all scenarios — if the user force-closes the app from recent apps, or the OS kills the process, no cleanup callbacks run. Do not rely on onDispose for critical data persistence. Use editor.storage.kv.setItem() incrementally to save important state.

Phase 6 -- Deactivation

After the JS runtime is torn down, the host performs comprehensive cleanup of all resources associated with the extension:

  • RPC state -- In-flight calls and buffered responses are cleared
  • Message bus -- All messaging channels for the extension are removed
  • Commands -- All commands and hooks registered by the extension are unregistered
  • Hook completers -- Any pending before-hook completers are resolved so commands are not left hanging
  • WebSocket connections -- All WebSocket connections owned by the extension are closed
  • UI elements -- Popups and bottom sheets belonging to the extension are dismissed
  • CodeMirror extensions -- Editor extensions contributed by the extension are removed from open tabs
  • Themes -- Themes registered by the extension are unregistered
  • Settings -- Setting schemas are unregistered
  • Contributions -- All contribution points are removed

Extension States

The host tracks which extensions are in which lifecycle phase:

StateDescription
ActiveExtensions with a running JS worker (onLoad completed, handling commands and events)
CrashedExtensions whose runtime errored or exited unexpectedly
RestartingExtensions currently in the restart flow (dispose then re-activate)

Execution Environment

Extensions run in isolated JavaScript environments powered by QuickJS. Each extension gets its own JS runtime in a background thread, which provides:

  • UI thread safety -- extensions cannot block the editor's UI
  • Crash isolation -- if one extension crashes, others continue running normally
  • Resource isolation -- each extension's JS state is completely separate

Heartbeat Watchdog

The host includes a heartbeat mechanism to detect unresponsive extensions. The host sends a liveness check every 5 seconds. If 3 consecutive heartbeats go unacknowledged (approximately 15--20 seconds), the extension is considered unresponsive and is automatically terminated. The extension is then moved to the "crashed" state and can be manually restarted.

Shutdown Behavior

When an extension is deactivated, the host uses a two-phase shutdown:

  1. Graceful shutdown (3-second window): The host signals the extension to run its onDispose callback and clean up resources.
  2. Forced shutdown (fallback): After 3 seconds, if the extension has not exited, it is force-terminated.

This ensures that well-behaved extensions have time to save state and release resources, while misbehaving extensions cannot block the shutdown process.

Crash Recovery

When an extension crashes (runtime error, unexpected exit, or heartbeat timeout), the host performs the following recovery steps:

  1. A [CRASHED] entry is added to the extension's log
  2. Full resource cleanup runs (see Phase 6 above)
  3. The extension is moved to the "crashed" state

The app automatically restarts crashed extensions. When the runtime detects that an extension's worker has terminated unexpectedly, it disposes the current worker with full cleanup, re-reads the manifest from the filesystem, and re-activates the extension automatically. All contributions are re-registered as part of this process.

All extensions can also be restarted at once, which fully re-bootstraps the extension system from the filesystem.

Console Logging

The host captures JavaScript console output (console.log, console.warn, console.error, console.info) from all extensions. Each log entry includes the extension ID, log level, message, and timestamp.

Key behaviors:

  • Maximum 1000 log entries are kept in memory (oldest entries are dropped when the limit is exceeded)
  • Log recording can be started and stopped (logs are not captured when recording is off)
  • Log entries can be viewed in real time through the extension console UI
  • The standard console.log(), console.warn(), console.error(), and console.info() functions work as expected in extension code — their output is captured and forwarded to the extension console

JS Lifecycle Callbacks

Extensions interact with the lifecycle through two SDK callbacks:

CallbackWhen CalledPurpose
editor.onLoad(callback)After the extension code is loadedInitialize state, register commands and event listeners
editor.onDispose(callback)Before the extension is shut downClean up resources, close connections, save state

Both callbacks can be async. The onLoad callback has a 30-second timeout, and the onDispose callback has a 3-second window before force termination.