Adds tests/background/sw-bundle-import.test.ts that loads the built SW
chunk under SW-simulated globals (Buffer/process/window/document stripped)
via a spawned Node child process. Pins the orchestrator-side gap that
caused Plan 01-08's SW init crash: the prior deps test only checked
SOURCE packages under default Node globals, never the bundled output, so
Vite/Rollup's CJS-interop bug (tree-shaking the `ebml` package while
leaving a dangling `{tools:f}=Pc` destructure against an empty Pc) went
undetected until operator empirical smoke.
RED against HEAD aabbd0c — failure surfaces the exact production error
("Cannot read properties of undefined (reading 'readVint')"), proving
the test is a true regression gate, not a tautology.
Also rewrites .planning/debug/01-08-sw-incompatibility.md to reflect the
actual root cause (Vite/Rollup CJS interop) rather than the orchestrator's
initial falsified hypothesis (new Function + Buffer globals — disproven
by Node simulation showing the throw fires at module-init line 12:33809
before any CSP-eval or Buffer-ref code path executes).
Full vitest: 60 passing + 3 RED (this gate + the 2 pre-existing Task 5
fixture-dependent duration tests). No regressions.
Per feedback-pre-checkpoint-bundle-gates.md (auto-loaded memory): any
future plan executor whose work surfaces a SW must run this test before
any operator-empirical checkpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
slug, status, trigger, created, updated, phase, related_plan, related_summary, related_uat, prior_resolved_sessions
| slug | status | trigger | created | updated | phase | related_plan | related_summary | related_uat | prior_resolved_sessions | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-08-sw-incompatibility | investigating | 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.
|
2026-05-17T07:34:32Z | 2026-05-17T08:15:00Z | 01-stabilize-video-pipeline | .planning/phases/01-stabilize-video-pipeline/01-08-PLAN.md | .planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md | .planning/phases/01-stabilize-video-pipeline/01-UAT.md |
|
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):
-
git checkout gsd/phase-01-stabilize-video-pipeline(HEAD:aabbd0c) -
npm install && npm run build -
Run a SW-simulated Node import:
node --input-type=module -e " delete globalThis.Buffer; delete globalThis.process; await import('./dist/assets/index.ts-8ny38Qcj.js'); " -
Observe identical crash to operator:
TypeError: Cannot read properties of undefined (reading 'readVint')
Reproduction (full smoke):
- Steps 1-2 above
KEEP_PROFILE=0 ./smoke.sh- In Chrome: Load Unpacked → dist/ — SW dies as described
Diagnostic evidence (bundle inspection):
$ 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):
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()→EBMLEncoderfactory (correctly bundled)Pc→ebmlpackage (empty placeholder; tree-shaken)Gc→ebml-block(correctly bundled, source visible)
Bundle source-identifier audit for ebml package:
$ 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 ints-ebml@3.0.2as a runtime dependency. - Deps test (Task 1, commit
5035314) wrongly certified SW-compat: it only checked source-leveldocument/windowreferences, 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:
- Tier 1 SW-bundle-import test passes.
- SW initializes cleanly in Chrome; chrome://serviceworker-internals shows Running Status: ACTIVATED, Fetch handler: EXISTS.
- Offscreen handshake completes.
- smoke.sh produces a zip with playable ~30s WebM.
- 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) andfeedback-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 verificationebml-stream— modern fork of node-ebml; may have similar CJS issueswebm-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 + remuxSegmentstests/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.jsline 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 applysrc/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')"throwsTypeError: 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.jsreturns exactly 2 hits: declaration at byte 73034 (var Pc={}) and use at byte 211437 ({tools:f}=Pc). Zero assignments between.Pcis the bundled identifier for the unresolvedebmlimport. -
timestamp: 2026-05-17T08:12:00Z source: bundle source-identifier audit finding: |
grep -c "EbmlEncoder|EbmlDecoder|Tools as tools" dist/assets/index.ts-8ny38Qcj.jsreturns 0/0/0. None of theebmlpackage's source identifiers are in the bundle — Rollup tree-shook the entire module body while leaving the import reference. By contrastint64-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), andbrowser: lib/ebml.iife.js(IIFE). Vite picksmodulefor 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 FunctionCSP-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 anynew FunctionorBuffer.fromcall 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. Thenew 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
ebmldep. -
"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: []