diff --git a/domains/platform/knowledge/extension-architecture.md b/domains/platform/knowledge/extension-architecture.md new file mode 100644 index 00000000..9b87656e --- /dev/null +++ b/domains/platform/knowledge/extension-architecture.md @@ -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) +│ └── .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`. diff --git a/domains/platform/knowledge/mv3-service-worker.md b/domains/platform/knowledge/mv3-service-worker.md new file mode 100644 index 00000000..5acdc7a1 --- /dev/null +++ b/domains/platform/knowledge/mv3-service-worker.md @@ -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 diff --git a/domains/platform/skills/extension-errors-debugging/repos/metamask-extension.md b/domains/platform/skills/extension-errors-debugging/repos/metamask-extension.md new file mode 100644 index 00000000..5020917c --- /dev/null +++ b/domains/platform/skills/extension-errors-debugging/repos/metamask-extension.md @@ -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. diff --git a/domains/platform/skills/extension-errors-debugging/skill.md b/domains/platform/skills/extension-errors-debugging/skill.md new file mode 100644 index 00000000..885f59f8 --- /dev/null +++ b/domains/platform/skills/extension-errors-debugging/skill.md @@ -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 | diff --git a/domains/platform/skills/extension-lifecycle-decoupling/skill.md b/domains/platform/skills/extension-lifecycle-decoupling/skill.md new file mode 100644 index 00000000..cd7e5950 --- /dev/null +++ b/domains/platform/skills/extension-lifecycle-decoupling/skill.md @@ -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 |