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 Event | Behavior |
|---|---|
onStartupFinished | Activated 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.
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:
| State | Description |
|---|---|
| Active | Extensions with a running JS worker (onLoad completed, handling commands and events) |
| Crashed | Extensions whose runtime errored or exited unexpectedly |
| Restarting | Extensions 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:
- Graceful shutdown (3-second window): The host signals the extension to run its
onDisposecallback and clean up resources. - 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:
- A
[CRASHED]entry is added to the extension's log - Full resource cleanup runs (see Phase 6 above)
- 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(), andconsole.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:
| Callback | When Called | Purpose |
|---|---|---|
editor.onLoad(callback) | After the extension code is loaded | Initialize state, register commands and event listeners |
editor.onDispose(callback) | Before the extension is shut down | Clean 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.