Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions domains/platform/knowledge/extension-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
name: extension-architecture
domain: platform
description: MetaMask extension — background/UI boundary, state sync, build types, key directories
---

# Extension Architecture

## Background / UI Boundary

The extension runs two separate JavaScript contexts that cannot share memory.

| Context | Entry | Access |
|---------|-------|--------|
| Background (Service Worker / background page) | `app/scripts/` | DOM-less; controllers, wallet logic |
| UI (popup/tab) | `ui/` | React + Redux; rendering only |
| Shared | `shared/` | Constants, utilities, type definitions |

Communication is message-based (Chrome runtime messaging). Code in `app/scripts/` cannot `import` from `ui/` and vice versa.

## State Sync Flow

```
Controller state changes (app/scripts/)
metamask-controller.js batches via debounce (200ms)
UI receives batched state via sendUpdate
Redux dispatches UPDATE_METAMASK_STATE
Immer applies patches (structural sharing — unchanged paths keep stable references)
useSelector evaluates; components re-render if output changed
```

Key file: `app/scripts/metamask-controller.js` — aggregates all controller state.

## Build Types

| Build | Command | Background | Security Policy |
|-------|---------|------------|-----------------|
| Development | `yarn start` | Webpack, hot reload | No LavaMoat |
| Production | `yarn dist` | Browserify | LavaMoat enforced |
| Test | `yarn build:test` | Browserify | Partial LavaMoat |

LavaMoat restricts package capabilities at runtime. After adding/updating dependencies, run `yarn lavamoat:auto` to regenerate policies.

## Manifest Versions

| Version | Background | Lifecycle |
|---------|------------|-----------|
| MV3 (Chrome) | Service Worker | Can terminate and restart |
| MV2 (Firefox) | Background Page | Always running |

Errors concentrated in MV3 (99%+) → root cause is service worker lifecycle, not application logic.

## Key Directories

```
app/scripts/
├── controllers/ # Feature controllers (one per domain)
├── lib/ # Background utilities
└── metamask-controller.js # Main aggregator; 200ms debounce

ui/
├── components/ # Reusable React components
├── pages/ # Page-level components
│ ├── routes/ # routes.component.tsx (high selector count)
│ └── home/ # home.container.js (legacy connect())
├── ducks/ # Redux slices
├── selectors/ # All selectors
│ ├── selectors.js # Main file (~2500 lines)
│ └── <feature>.ts # Feature-specific selectors
└── contexts/ # React Context providers

shared/
├── constants/
├── lib/
└── modules/
└── selectors/
└── selector-creators.ts
```

## React Compiler Scope

Enabled for `ui/components`, `ui/contexts`, `ui/hooks`, `ui/layouts`, `ui/pages`.

Does NOT cross file boundaries — selector values from `useSelector` require manual `useMemo`.
94 changes: 94 additions & 0 deletions domains/platform/knowledge/mv3-service-worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
name: mv3-service-worker
domain: platform
description: MV3 service worker lifecycle — Chrome background termination model, MetaMask's idle-termination mitigation, and cold-start failure modes
---

# MV3 Service Worker Lifecycle

## MV2 vs MV3

| Manifest | Background | Default Lifecycle | Mitigated in MetaMask? |
|----------|------------|-------------------|------------------------|
| MV3 (Chrome) | Service Worker | Idle termination after 30s, hard cap ~5 min | Yes — see Idle Termination Mitigation |
| MV2 (Firefox) | Background Page | Always running | N/A |

## Idle Termination Mitigation

`app/scripts/background.js:750-758` runs a 2s `browser.storage.session` write loop. `saveTimestamp` (defined at `background.js:651-655`) writes an ISO timestamp into session storage:

function saveTimestamp() {
const timestamp = new Date().toISOString();
browser.storage.session.set({ timestamp });
}
...
const SAVE_TIMESTAMP_INTERVAL_MS = 2 * 1000;
saveTimestamp();
setInterval(saveTimestamp, SAVE_TIMESTAMP_INTERVAL_MS);

Each `chrome.*` / `browser.*` API call resets the 30s idle timer. At 2s cadence the worker stays alive indefinitely while the extension is active. `storage.session` (not `storage.local`) is deliberate — it is MV3-only, in-memory, and does not accumulate disk writes from a heartbeat.

| Property | Value |
|---|---|
| API | `browser.storage.session.set` (MV3-only, in-memory) |
| Interval | 2000 ms (`SAVE_TIMESTAMP_INTERVAL_MS`) |
| Gate | `PreferencesController.enableMV3TimestampSave !== false` (default true) |
| Inline comment | `background.js:752` — "This keeps the service worker alive" |
| Pattern origin | De facto community consensus, not officially endorsed by Chrome DevRel |
| Re-verify if | Chromium policy change on idle-timer API interactions |

Ongoing idle termination is **not** a live failure mode while the extension is running. Cold starts (browser launch, extension enable/reload, crash recovery) are the actual source of MV3-concentrated failures.

## Verification Discipline

Before attributing an MV3-concentrated error to "idle termination pressure":

1. Verify `background.js:750-758` keepalive loop still exists and `saveTimestamp` still calls a `chrome.*` / `browser.*` API
2. Verify `enableMV3TimestampSave` is not disabled in affected Sentry events
3. Check whether error timing correlates with cold-start events, not idle periods

If any check out, the working hypothesis is cold-start cascade race, not ongoing termination.

## Error Concentration Signal

| Distribution | Conclusion |
|---|---|
| ~50/50 MV3/MV2 | Application bug (affects both contexts equally) |
| 99%+ MV3 only | MV3 service worker lifecycle — check cold-start cascade before assuming idle termination |
| 99%+ MV2 only | Firefox-specific browser behavior |

## Sentry Tag Dimensions

Independent — do not conflate.

| Tag | Meaning |
|-----|---------|
| `environment` | Build configuration (production, staging, development) |
| `installType` | How extension was loaded (normal, development, sideload, admin) |
| `dist` | Manifest version (mv3, mv2) |

A production build can have `installType: development` if loaded unpacked. Filter carefully.

## MV3-Specific Failure Modes

| Failure | Cause | Mitigated? |
|---------|-------|------------|
| Cold-start cascade race (`APP_INIT_ALIVE` sent before UI listener bound) | `app-init.js` → dynamic-import `background.js` → listener registration races against an open port | No |
| `Background connection unresponsive` via ongoing idle termination | Worker idle-killed mid-session | Yes — 2s keepalive loop |
| `Background connection unresponsive` via cold-start latency | Cold start on browser launch + first-flush latency before `startUiSync` | No — keepalive does not apply before worker exists |
| Silent `postMessage` failure | Port disconnected during wake/termination, try/catch swallows error | No |
| In-memory state lost on cold start | New worker instance has empty in-memory state | No (fresh persistence read required) |

## Sentry Diagnostic Instrumentation

| Tag | Purpose | Status |
|-----|---------|--------|
| `uiStartup.receivedAppInitPing` | Distinguishes cold-start cascade race cases; `false` + `ALIVE` received ⇒ `APP_INIT_ALIVE` lost on cold start | Missing on `Background connection unresponsive` path as of 13.26.0 — instrumentation gap, being fixed |
| Phase-specific critical error types (`BACKGROUND_INITIALIZED`, `START_UI_SYNC`) | Distinguishes which startup phase hung | Added by 3-phase startup watchdog (PR #40306) |

## When to Investigate MV3 Separately

- Error volume is 10× higher in Chrome than Firefox
- Error involves background connectivity, keepalive, or startup handshake
- Error disappears when running with the worker kept alive manually
- Error correlates with browser-launch or extension-reload timestamps, not idle gaps
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
repo: metamask-extension
parent: extension-errors-debugging
---

## Sentry Filters

Filter by `dist` tag to isolate manifest version:
- `dist:mv3` — Chrome builds
- `dist:mv2` — Firefox builds

Filter by `installType` to exclude developer-loaded builds:
- `installType:normal` — store-installed
- `installType:development` — sideloaded (unpacked); includes production builds loaded via developer mode

## Build Commands

```bash
# MV3 development (Chrome, service worker)
yarn start

# MV2 development (Firefox, background page)
yarn start:mv2

# Production build (both manifests)
yarn dist

# After dependency changes — regenerate LavaMoat policies
yarn lavamoat:auto
```

## Background Keepalive

| Property | Value |
|---|---|
| Location | `app/scripts/background.js:750-758` |
| Function | `saveTimestamp` at `background.js:651-655` calls `browser.storage.session.set({ timestamp })` |
| Cadence | 2000 ms via `setInterval` |
| Effect | Each call resets Chrome's 30s SW idle timer — prevents idle eviction during active sessions |
| Gate | `PreferencesController.enableMV3TimestampSave !== false` |

Active-session keepalive failures are rare and should be investigated as code bugs, not platform behavior. Cold-start cascade and first-flush latency are the actual MV3-concentrated failure modes — see `mv3-service-worker` knowledge for mechanism, failure modes table, and verification discipline.

## Controller-Messenger Pattern

Controllers communicate via `ControllerMessenger` (`@metamask/base-controller`). A controller's public API is its registered actions and events — not direct method calls. Cross-controller calls that bypass the messenger will not work across the background/UI boundary.
59 changes: 59 additions & 0 deletions domains/platform/skills/extension-errors-debugging/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
maturity: experimental
name: extension-errors-debugging
description: Diagnose browser extension errors — MV3 vs MV2, background/UI context, error tagging
---

# Extension Errors Debugging

## When To Use

- Errors appear in one manifest version but not the other
- Background connection or keepalive failures
- Errors that are hard to reproduce in development (only manifest in prod)
- Diagnosing Sentry errors before attributing root cause

## Do Not Use When

- Local development errors with full stack traces and reliable repro
- Build/compile errors (TypeScript, ESLint, bundler)
- Test failures unrelated to extension runtime behavior

## Workflow

1. **Check distribution** — Filter by `dist` tag. Is the error 99%+ MV3, MV2, or split?
2. **Classify root cause** — MV3-only → service worker lifecycle (specifically cold-start cascade; ongoing idle termination is mitigated — see `mv3-service-worker` knowledge). Split → application logic. MV2-only → Firefox behavior.
3. **Identify context** — Is the error from background (`app/scripts/`) or UI (`ui/`)? Stack trace file paths reveal this.
4. **Check error tags** — Verify `environment`, `installType`, and `dist` are what you expect (these are independent dimensions).
5. **Reproduce** — Use `dist` tag filter to reproduce in the right manifest version.

## Context Identification from Stack Traces

| Path prefix in trace | Context |
|---------------------|---------|
| `app/scripts/controllers/` | Background controller |
| `app/scripts/metamask-controller.js` | Background aggregator |
| `ui/components/` or `ui/pages/` | UI (React) |
| `shared/` | Either — shared module |

## Background-Specific Error Types

| Error | MV3 Root Cause | Mitigated? |
|-------|---------------|------------|
| Background connection unresponsive (cold-start cascade) | `app-init.js` → `background.js` listener race on worker cold start | No |
| Background connection unresponsive (first-flush latency) | Cold start + background state aggregation before `startUiSync` | No |
| Background connection unresponsive (idle termination) | Worker idle-killed mid-session | Yes — 2s `browser.storage.session` keepalive |
| Port disconnected (wake/termination race) | Port closed during worker lifecycle transition; silent via try/catch | No |
| Keepalive timer missed (active session) | Would imply `browser.storage.session.set` interval failed — rare; investigate as application bug, not platform behavior | N/A |
| In-memory state lost (cold start) | New worker instance re-reads persisted state | No |

## Common Pitfalls

| Mistake | Correct Approach |
|---------|-----------------|
| Attribute 99% MV3 error to application code | Check if error requires running background; MV3 SW lifecycle is the likely root cause |
| Default to "SW was terminated mid-session" for MV3 errors | Ongoing idle termination is mitigated by the 2s `browser.storage.session` keepalive. The likely mechanism is cold-start cascade or first-flush latency — see `mv3-service-worker` knowledge |
| "Keepalive timer missed" ⇒ SW slept | The 2s keepalive prevents idle sleep while active. A missed keepalive during active session is a code bug, not platform behavior |
| Use `environment` to filter for dev builds | Use `installType: development` — a prod build can be sideloaded |
| Conflate `dist` and `environment` | They are independent; filter both when needed |
| Reproduce MV2-only error in Chrome | Use Firefox; `installType` doesn't replicate MV3/MV2 lifecycle difference |
84 changes: 84 additions & 0 deletions domains/platform/skills/extension-lifecycle-decoupling/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
maturity: experimental
name: extension-lifecycle-decoupling
description: Verify platform lifecycle events before assuming they cause application-level side effects
---

# Extension Lifecycle Decoupling

## When To Use

- Estimating event frequency based on service worker eviction
- Debugging behavior that "should" trigger on lock/unlock but doesn't
- Investigating keepalive, timer, or state persistence behavior

## Do Not Use When

- Working on UI-only code with no background process interaction
- The behavior reproduces reliably in development without service worker eviction

## Core Distinction

| Layer | Examples | Characteristics |
|-------|---------|----------------|
| Platform lifecycle | SW eviction, page unload | Infrastructure-level |
| Application lifecycle | Lock, unlock, init | User-level |

These layers are often **decoupled**. The mapping between them is an implementation detail — verify it, don't assume it.

## Verification Checklist

Before claiming a platform lifecycle event causes application behavior:

1. Is there an explicit handler (`onSuspend`, `beforeunload`) that triggers the claimed effect?
2. Is there a keepalive mechanism preventing the lifecycle event?
3. Does relevant state persist across restarts (`chrome.storage.session`, IndexedDB)?
4. Are timers alarm-based (persist across SW restart) or `setTimeout`-based (don't)?
5. Is the guard/flag reset by the lifecycle event or by a separate application event?

## MV3 MetaMask Specifics

| Assumption | Reality |
|------------|---------|
| SW eviction triggers lock | No `onSuspend` lock handler — SW eviction does NOT trigger lock |
| Timers lost on SW restart | Auto-lock uses Chrome Alarms API — persists across SW restarts |
| State lost on SW restart | Wallet state persists in `chrome.storage.session` and IndexedDB |
| SW evicts frequently during active use | A keepalive writes `browser.storage.session.set` on a short interval, and each `chrome.*`/`browser.*` call resets the 30s idle timer — so active-session eviction is effectively prevented. Cold starts (browser launch, extension reload) still happen. **Re-verify before relying on it — see below.** See `mv3-service-worker` knowledge for mechanism and verification discipline |

### Re-verify the keepalive before reasoning from it

This row is the only one that depends on a *current implementation detail* rather than on
absent handlers or persistent storage, and it is the one that inverts if the implementation
moves. If the interval grows past the idle timeout, or the keepalive is removed, the honest
answer flips from "eviction is prevented" to "eviction happens routinely" — and a skill that
still asserts the first would be worse than no skill.

Confirm it in the target repo before drawing conclusions:

```bash
# the keepalive writer and its cadence — symbol names, not line numbers
grep -rn "saveTimestamp\|SAVE_TIMESTAMP_INTERVAL_MS" app/scripts/background.js
```

Two things make the conclusion hold, and both must still be true:

1. The interval is **well under the ~30s idle timeout** (last verified: `2 * 1000` ms).
2. The callback performs an **extension API call** — `browser.storage.session.set` — since it
is the API call that resets the timer, not the timer firing.

If either has changed, treat active-session eviction as live and re-derive the rest of this
table's consequences.

*Verified against `metamask-extension` at `d4dd55f300a` (2026-07-30):
`SAVE_TIMESTAMP_INTERVAL_MS = 2 * 1000`, `setInterval(saveTimestamp, …)`,
`saveTimestamp` calling `browser.storage.session.set`.*

## Common Pitfalls

| Mistake | Correct Approach |
|---------|-----------------|
| "SW evicts N times/day → event fires N times/day" | Check if application code has handler for eviction |
| Assume frequency from platform behavior | Grep for actual handler chains in `background.js`, `app-state-controller.ts` |
| Conflate platform restart with application reset | Check which state is persisted vs re-initialized |
| "Keepalive uses `chrome.alarms`" | It does not — keepalive works by making an extension API call (`browser.storage.session.set`) on a sub-idle-timeout interval. `chrome.alarms` is used separately, for auto-lock timers that must persist across SW restart |
| Citing this skill's keepalive claim without re-checking | It is the one row here that tracks a live implementation detail. Run the grep above; the conclusion inverts if the interval or the API call changes |