--- slug: 01-08-sw-incompatibility status: investigating trigger: | Plan 01-08 Tasks 1-4 landed cleanly (5 commits 5035314..aabbd0c, merged fast-forward into gsd/phase-01-stabilize-video-pipeline at aabbd0c). All gates green: tsc clean, type-safety grep clean, npm run build exit 0, 60/62 vitest GREEN (only the 2 fixture-dependent webm-playback duration tests remain RED — those are Task 5's empirical responsibility). Operator ran smoke.sh against the post-remux build and reported: "it errored, and i can't even see the SW console" — chrome://serviceworker-internals shows the SW at Running Status: STARTING (stuck forever), Fetch handler existence: DOES_NOT_EXIST, Log empty. The SW dies at top-level module evaluation BEFORE any handler registers and before any console.log can fire. Initial orchestrator hypothesis ("ts-ebml uses `new Function` + Buffer globals → CSP-blocks SW") was speculation from bundle grep and proved WRONG when tested. A proper Node-simulation that strips SW-relevant globals (`delete globalThis.Buffer; delete globalThis.process; await import('./dist/assets/index.ts-8ny38Qcj.js')`) reveals the actual error fires at top-level module init: TypeError: Cannot read properties of undefined (reading 'readVint') at file:///.../dist/assets/index.ts-8ny38Qcj.js:12:33809 at hn (file:///.../dist/assets/index.ts-8ny38Qcj.js:12:41461) at file:///.../dist/assets/index.ts-8ny38Qcj.js:12:42172 at ModuleJob.run (node:internal/modules/esm/module_job:430:25) Bundle context at the failure site: i.readVint=i.writeVint=i.readBlock=...=void 0; const s=mo, h=a(go()), {tools:f}=Pc, d=Gc; i.readVint = f.readVint; // ← throws: f is undefined This is the bundled form of ts-ebml/lib/tools.js. The destructure `{tools:f}=Pc` fails because `Pc` is an empty placeholder namespace object (`var Pc={}` — declared once, never populated). `Pc` is the Vite/Rollup-mangled identifier for the `ebml` package (transitive dep of ts-ebml; ts-ebml's tools.js does `const { tools: _tools } = require("ebml")`). Root cause is a Vite/Rollup CJS-interop bug, NOT a SW-API mismatch. ts-ebml itself is structurally SW-compatible; it just cannot find its transitive `ebml` dependency at runtime because Rollup tree-shook the entire ebml module body while leaving a placeholder reference behind. The CSP-eval and Buffer-global concerns from the original hypothesis are real (they would have fired AFTER this error) but are downstream of the actual init-time crash. Plan 01-08's Task 1 deps-compatibility test (tests/background/ webm-remux-deps.test.ts) ran in vitest's Node env where Buffer IS defined and inspected source files for DOM globals — it never loaded the bundled output in a SW-simulated env, so the runtime tree-shake hole and the SW-global stripping were both missed. This blocks Plan 01-08 entirely until the bundle either successfully imports `ebml` or replaces ts-ebml with something Vite-friendly. created: 2026-05-17T07:34:32Z updated: 2026-05-17T08:15:00Z phase: 01-stabilize-video-pipeline related_plan: .planning/phases/01-stabilize-video-pipeline/01-08-PLAN.md related_summary: .planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md related_uat: .planning/phases/01-stabilize-video-pipeline/01-UAT.md prior_resolved_sessions: - .planning/debug/resolved/d12-blob-port-transfer-fails.md - .planning/debug/resolved/webm-playback-freeze.md - .planning/debug/resolved/empty-archive-port-race.md - .planning/debug/d13-multi-ebml-concat-unplayable.md (the prior bug that Plan 01-08 was supposed to fix; still open until 01-08 actually works) --- # Debug: Plan 01-08 SW init crash — Vite/Rollup CJS interop strips `ebml` from bundle ## Symptoms **Expected:** SW initializes cleanly; chrome://extensions shows the "service worker" link active; SW console accessible; offscreen handshake completes; recording starts. **Actual:** SW dies at top-level module evaluation; chrome://serviceworker-internals: Status=STARTING (stuck), Fetch handler=DOES_NOT_EXIST, Log=empty. Operator cannot reach the SW console because no handler ever registers. **Reproduction (bundle-level, no Chrome needed):** 1. `git checkout gsd/phase-01-stabilize-video-pipeline` (HEAD: aabbd0c) 2. `npm install && npm run build` 3. Run a SW-simulated Node import: ```bash node --input-type=module -e " delete globalThis.Buffer; delete globalThis.process; await import('./dist/assets/index.ts-8ny38Qcj.js'); " ``` 4. Observe identical crash to operator: `TypeError: Cannot read properties of undefined (reading 'readVint')` **Reproduction (full smoke):** 1. Steps 1-2 above 2. `KEEP_PROFILE=0 ./smoke.sh` 3. In Chrome: Load Unpacked → dist/ — SW dies as described **Diagnostic evidence (bundle inspection):** ```bash $ grep -boE "\bPc\b" dist/assets/index.ts-8ny38Qcj.js 73034:Pc # ← declaration 211437:Pc # ← only use site (the failing destructure) ``` Bytes 72950-73050: `...var Pc={},Zi={exports:{}},Xi={exports:{}}...` — `Pc` is declared as an empty object literal and **never assigned** anywhere else in the 374 KB bundle. Bytes 211350-211450 (failure site, transpiled `ts-ebml/lib/tools.js`): ```js const s=mo, h=a(go()), {tools:f}=Pc, d=Gc; i.readVint = f.readVint; // ← throws here at module init ``` Identifier mapping (verified against `node_modules/ts-ebml/lib/tools.js`): - `mo` → `int64-buffer` (correctly bundled, source visible) - `go()` → `EBMLEncoder` factory (correctly bundled) - `Pc` → `ebml` package (empty placeholder; tree-shaken) - `Gc` → `ebml-block` (correctly bundled, source visible) Bundle source-identifier audit for `ebml` package: ```bash $ grep -c "EbmlEncoder" dist/assets/index.ts-8ny38Qcj.js 0 $ grep -c "EbmlDecoder" dist/assets/index.ts-8ny38Qcj.js 0 $ grep -c "Tools as tools" dist/assets/index.ts-8ny38Qcj.js 0 ``` None of the `ebml` package's source identifiers appear in the bundle — Rollup tree-shook the entire module body while leaving the destructure reference dangling. **Why the CJS interop fails:** `node_modules/ebml/package.json` declares all three of `main`, `module`, and `browser`. Vite (browser/SW target) prefers `module` (`lib/ebml.esm.js`), which exports as **named ESM**: export { Tools as tools, schema, EbmlDecoder as Decoder, EbmlEncoder as Encoder }; But `node_modules/ts-ebml/lib/tools.js` (compiled CJS) does: const { tools: _tools } = require("ebml"); `@rollup/plugin-commonjs` is supposed to bridge a CJS `require()` of an ESM module by wrapping it. Here it allocated the namespace placeholder `var Pc = {}` for the would-be `module.exports`, but the wrapper that should rewrite it via `Pc.tools = Tools; Pc.schema = schema; ...` was never emitted. Body of `ebml.esm.js` was tree-shaken because Rollup could not statically prove `Pc.readVint`/`Pc.writeVint` reach the public surface (they're funneled through ts-ebml's `_tools` local). This is a known class of @rollup/plugin-commonjs failure mode for packages that mix `module`/`main`/`browser` fields with consumers that require them via CJS; usually fixed by forcing esbuild's CJS-interop via `optimizeDeps.include` or by tightening `commonjsOptions`. **Timeline:** - Bug introduced: commit 41e94d5 ("feat(01-08): implement remuxSegments") pulled in `ts-ebml@3.0.2` as a runtime dependency. - Deps test (Task 1, commit 5035314) wrongly certified SW-compat: it only checked source-level `document`/`window` references, not bundle-level import-load behavior in a SW-simulated env. - Discovered: 2026-05-17 by operator empirical smoke. - Initial orchestrator hypothesis (new Function + Buffer) FALSIFIED 2026-05-17 via Node-simulation; real cause identified the same day. ## Current Focus hypothesis: | Vite/Rollup's default CJS-interop pipeline tree-shakes the `ebml` package out of the SW bundle while leaving a dangling destructure reference in the bundled `ts-ebml/lib/tools.js`. At SW init time the destructure `{tools:f}=Pc` evaluates to `{tools: undefined}` because `Pc` is an empty placeholder namespace object that the CJS wrapper never populates. Then `_tools.readVint` throws TypeError at module-level execution, killing the SW before any handler registers. This is NOT a ts-ebml-vs-SW-API mismatch, NOT a CSP eval issue, NOT a Buffer-global issue. Those concerns were the orchestrator's initial speculative hypothesis and are FALSIFIED by the Node simulation — the crash fires before any of those code paths would execute. (They may surface as secondary issues once the primary is fixed; the strengthened RED gate must catch those too.) The fix space is bundler-configuration vs library-swap vs architectural relocation. See "Candidate fix strategies" below. test: | Two-tier RED gate, both required: Tier 1 (cheap, deterministic, runs in vitest): load the built SW bundle via `await import(distPath)` after stripping SW-incompatible globals (`delete globalThis.Buffer; delete globalThis.process; delete globalThis.document; delete globalThis.window`). Assert no throw. Lives at `tests/background/sw-bundle-import.test.ts`. This is the gate that should have caught this bug pre-checkpoint. Tier 2 (optional, expensive): playwright + a real Chrome MV3 unpacked-load that checks the SW reaches OFFSCREEN_READY. Deferred unless Tier 1 proves insufficient. Tier 1 will go RED IMMEDIATELY against the current dist bundle. It will go GREEN only after the chosen fix lands. expecting: | After fix lands: 1. Tier 1 SW-bundle-import test passes. 2. SW initializes cleanly in Chrome; chrome://serviceworker-internals shows Running Status: ACTIVATED, Fetch handler: EXISTS. 3. Offscreen handshake completes. 4. smoke.sh produces a zip with playable ~30s WebM. 5. The 2 currently-RED webm-playback duration tests (Task 5's gate) either go GREEN or surface a separate, post-fix issue worth debugging on its own merits. next_action: | CHECKPOINT to orchestrator with the 4 candidate fix strategies + the debugger's recommendation. Orchestrator routes to user via AskUserQuestion. Per feedback-no-unilateral-scope-reduction, the debugger does NOT pick. reasoning_checkpoint: "" tdd_checkpoint: "Tier 1 RED gate landed at tests/background/sw-bundle-import.test.ts — verified RED against HEAD aabbd0c" ## Constraints - TDD mode is ON. Tier 1 RED test landed BEFORE any GREEN fix. - Auto-loaded memories: `feedback-gsd-ceremony-for-fixes.md` (no hot-edits) and `feedback-no-unilateral-scope-reduction.md` (no scope narrowing; surface choices via AskUserQuestion). - `feedback-pre-checkpoint-bundle-gates.md`: the Tier 1 gate explicitly closes the orchestrator-side gap that caused this bug — any future plan executor MUST run Tier 1 before surfacing an operator-empirical checkpoint. - Plan 01-08 Tasks 1-4 are committed (5 commits). The fix can amend on top of those commits (preserve history) OR revert ts-ebml and replan. Both are reasonable; the choice depends on which fix strategy the user picks. - The pre-existing deps test (tests/background/webm-remux-deps.test.ts) is INSUFFICIENT; the new Tier 1 gate supersedes it. Whether to delete or rename the old one is a follow-up — keep it for now. - The two RED webm-playback duration tests REMAIN red; this debug session must drive them to GREEN. ## Candidate fix strategies (surface to user; debugger does NOT pick) ### Strategy A — Vite `optimizeDeps.include: ['ts-ebml', 'ebml']` **Mechanism:** Force esbuild to pre-bundle `ts-ebml` + `ebml` during Vite's dep-optimization phase. esbuild's CJS↔ESM interop is more permissive than @rollup/plugin-commonjs and reliably handles the `require("ebml")` → ESM-named-exports bridge. **Blast radius:** Tiny — adds 2 lines to vite.config.ts. No src/ changes. No dep changes. Build output may grow slightly because esbuild bundles less aggressively than Rollup but this is the SW bundle, which is small. **Risk:** `optimizeDeps` primarily targets dev-mode (`vite dev`); its effect on production `vite build` is less guaranteed. May need to pair with `build.commonjsOptions` (Strategy B). Worth testing in isolation first. **Effort:** 30 min including verification. ### Strategy B — Vite `build.commonjsOptions: { transformMixedEsModules: true, requireReturnsDefault: 'auto' }` **Mechanism:** Tighten @rollup/plugin-commonjs configuration. `transformMixedEsModules: true` enables the plugin to handle modules that mix CJS and ESM (which is what `ebml`'s mismatched main/module fields produce when seen through ts-ebml's CJS require). `auto` requireReturnsDefault picks the right shape per-module. **Blast radius:** Same as A — 2 lines in vite.config.ts. May combine with A. **Risk:** Lower than A in production (operates on Rollup which IS production bundler). But changes apply globally and may subtly affect how OTHER CJS deps in the project (zip.js, etc.) bundle. Needs a full vitest re-run. **Effort:** 30 min including verification. ### Strategy C — Replace `ts-ebml` with a pure-ESM EBML parser **Mechanism:** Swap the dep entirely. Candidates: - `jswebm` — pure-ESM WebM parser; smaller surface; needs API verification - `ebml-stream` — modern fork of node-ebml; may have similar CJS issues - `webm-cluster-parser` — narrow-scope parser; might fit our needs - Hand-rolled minimal EBML reader for just the 3 element types we need (Segment, Cluster, SimpleBlock) — maybe ~200 LOC **Blast radius:** Large — rewrite of `src/background/webm-remux.ts` + all unit tests that mock ts-ebml. Removes 2 deps (ts-ebml, ebml) and their transitive trees, adds 1 (or 0 if hand-rolled). **Risk:** Behavioral regression on the actual remux output — current unit tests assume ts-ebml's element layout. Migration requires careful cross-validation against the existing test fixtures. Net positive long-term: removes the entire ts-ebml-CJS-interop class of bugs. **Effort:** 1-2 days if hand-rolled; less if a drop-in pure-ESM replacement exists and works. ### Strategy D — Move EBML parsing to OFFSCREEN document **Mechanism:** OFFSCREEN has full DOM, lenient CSP, and standard ESM/CJS interop because Vite emits a separate offscreen bundle that goes through a different (more permissive) loader path. Move `remuxSegments` from `src/background/webm-remux.ts` to a new `src/offscreen/remux.ts`; the SW posts segments to offscreen via chrome.runtime.sendMessage and gets the remuxed Blob back. **Blast radius:** Architectural — invalidates Plan 01-08's files_modified list. Requires Plan 01-08 amendment. May touch Plan 01-09's `src/offscreen/recorder.ts` for handler co-location. Adds a new SW↔offscreen message type. **Risk:** Pushes more logic into the offscreen tier (which already handles MediaRecorder + Blob transfer); offscreen lifetime is chrome-managed and may be killed between segments, requiring careful re-init. Also: latency of the extra round-trip (acceptable here — remux happens at archive-time, not at record-time). **Effort:** ~1 day including re-coordination with Plan 01-09. ### Debugger recommendation **Try A first (30 min), fall back to B (30 min), fall back to C (1-2 days), fall back to D (1 day).** Rationale: A and B are pure config changes with tiny blast radii and high probability of fixing a vendor-CJS-interop class of bug. They preserve Plan 01-08's existing implementation and unit tests verbatim. C and D are heavier-weight backstops only justified if A and B both fail. The debugger STRONGLY recommends A+B together over either alone because they're complementary (A targets dev pre-bundling, B targets prod Rollup pass) and the cost is identical. ## Files of Interest - `src/background/webm-remux.ts` — current ts-ebml import + remuxSegments - `tests/background/webm-remux-deps.test.ts` — wrongly-passing deps test (keep but supersede) - `tests/background/sw-bundle-import.test.ts` — NEW Tier 1 RED gate (this session) - `dist/assets/index.ts-8ny38Qcj.js` — broken SW bundle (diagnostic only) - `node_modules/ts-ebml/lib/tools.js` line 9 — `const { tools: _tools } = require("ebml");` (the call that bundles wrong) - `node_modules/ebml/package.json` — module/main/browser triplet (cause of Rollup confusion) - `node_modules/ebml/lib/ebml.esm.js` — what Vite picked (named exports) - `node_modules/ebml/lib/ebml.js` — what ts-ebml's CJS require expects (default export) - `vite.config.ts` — where strategies A and B would apply - `src/background/index.ts` — createArchive call site (importer) ## Evidence - timestamp: 2026-05-17T08:10:00Z source: Node SW-simulation finding: | `node --input-type=module -e "delete globalThis.Buffer; delete globalThis.process; await import('./dist/assets/index.ts-8ny38Qcj.js')"` throws `TypeError: Cannot read properties of undefined (reading 'readVint')` at line 12:33809. Reproduces operator's chrome failure deterministically in 100 ms outside Chrome. - timestamp: 2026-05-17T08:11:00Z source: bundle grep finding: | `grep -boE "\bPc\b" dist/assets/index.ts-8ny38Qcj.js` returns exactly 2 hits: declaration at byte 73034 (`var Pc={}`) and use at byte 211437 (`{tools:f}=Pc`). Zero assignments between. `Pc` is the bundled identifier for the unresolved `ebml` import. - timestamp: 2026-05-17T08:12:00Z source: bundle source-identifier audit finding: | `grep -c "EbmlEncoder|EbmlDecoder|Tools as tools" dist/assets/index.ts-8ny38Qcj.js` returns 0/0/0. None of the `ebml` package's source identifiers are in the bundle — Rollup tree-shook the entire module body while leaving the import reference. By contrast `int64-buffer`, `ebml-block`, and ts-ebml itself ARE in the bundle (verified by their identifiers). - timestamp: 2026-05-17T08:13:00Z source: ts-ebml/lib/tools.js inspection finding: | Line 9: `const { tools: _tools } = require("ebml");`. Line 11: `exports.readVint = _tools.readVint;`. This is the exact pattern that Vite/Rollup bundles into `{tools:f}=Pc; i.readVint=f.readVint`. - timestamp: 2026-05-17T08:14:00Z source: node_modules/ebml/package.json finding: | Declares `main: lib/ebml.js` (CJS, default-exports-style), `module: lib/ebml.esm.js` (ESM named exports), and `browser: lib/ebml.iife.js` (IIFE). Vite picks `module` for the browser/SW target. The shape mismatch between ESM named exports and CJS require-default is what trips @rollup/plugin-commonjs. - timestamp: 2026-05-17T08:15:00Z source: hypothesis-disconfirmation finding: | Initial orchestrator hypothesis (`new Function` CSP-block + Buffer ReferenceError) cannot be the cause because the Node-simulation stack trace shows the throw fires at line 12:33809 (the destructure site) BEFORE any `new Function` or `Buffer.from` call executes. Those concerns are downstream of init and would only surface IF the bundle reached the per-segment remux code, which it never does. The original hypothesis is FALSIFIED. ## Eliminated - "ts-ebml uses `new Function`, blocked by SW CSP" — FALSIFIED. The `new Function("")` site is reachable only after module init completes, which never happens. CSP block is downstream. - "ts-ebml uses `Buffer.from`, undefined in SW" — FALSIFIED for the init crash. Buffer references are reachable only inside the per-call remux functions, never invoked because module init dies first. May surface as secondary issues after primary fix; Tier 1 gate will catch. - "ts-ebml itself is SW-incompatible" — FALSIFIED. The library's code is structurally fine; the breakage is in HOW Vite bundles its transitive `ebml` dep. - "Plan 01-08 implementation bug in src/background/webm-remux.ts" — FALSIFIED. The crash is in bundled node_modules code, not in application src/. The Plan 01-08 implementation is fine. ## Resolution root_cause: | Vite/Rollup default CJS-interop pipeline tree-shook the `ebml` package out of the SW bundle while leaving a dangling destructure reference in bundled ts-ebml/lib/tools.js. The destructure `{tools:f}=Pc` against an empty placeholder `Pc` throws TypeError at SW top-level module init, killing the SW before any handler can register. Caused by `ebml`'s mismatched main/module/browser package fields colliding with ts-ebml's CJS-style `require("ebml")` import. fix: "" verification: "" files_changed: []