docs(debug): SC#1 sw-offscreen-persistence investigation — INCONCLUSIVE

Pre-commit-ceremony verification of Plan 04-04 Wave 0 SPIKE finding
(videoSize=8505 bytes after 5-min SW idle + Puppeteer worker.close()).

Reproducibility: 4/4 runs (incl. prior 3726eee) produced identical
8505-byte WebM. Deterministic.

Chrome docs research: chrome.offscreen DISPLAY_MEDIA reason has NO
lifetime limit; offscreen "may outlive" its SW; Puppeteer #9995 +
crbug 1371432 document CDP attach distorting SW lifecycle; chromium
auto-throttled-screen-capture + Chrome Bug 653548 document canvas-
captureStream throttling on invisible/background tabs.

Verdict: INCONCLUSIVE — the spike's 8505-byte result is consistent
with THREE competing root causes (test-invalid headless throttling;
CDP-artifact collateral teardown; architectural offscreen-RAM-loss)
and the spike cannot disambiguate between them. Observability gaps:
launch.ts:225 filters offscreen console on background_page (MV2)
when MV3 offscreen is type 'page' → zero offscreen logs in all spike
runs.

Recommendation: PAUSE the ~2-4h IndexedDB plan-fix. Three cheap
disambiguation steps (~75 min total) can isolate the actual root
cause before committing. Detailed in the debug note's
routing_recommendation block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:12:46 +02:00
parent e8a2e7696d
commit d614462694

View File

@@ -0,0 +1,180 @@
---
status: diagnosed
trigger: "Verify Plan 04-04 Wave 0 SPIKE empirical finding before committing multi-hour plan-fix work (IndexedDB-in-offscreen persistence). Use scientific method: re-run spike for reproducibility, deeply investigate Chrome MV3 offscreen lifecycle docs, produce routing recommendation."
created: 2026-05-21T19:00:00Z
updated: 2026-05-21T19:15:00Z
verdict: INCONCLUSIVE
recommendation: "Pause plan-fix work; augment spike observability OR test against real (non-fake) capture path before committing to IndexedDB persistence. The spike's failure mode is reproducible but observability-limited; multiple competing root causes are all consistent with the evidence. Committing to IndexedDB persistence based on this single-observability-channel result risks solving the wrong problem."
---
## Current Focus
verdict: INCONCLUSIVE (lean: original architectural-failure hypothesis is NOT empirically demonstrated by this spike; the spike conflates ≥3 competing failure modes)
hypothesis_state: "Multiple competing root causes, each consistent with the observed 8505-byte deterministic result. Cannot recommend ~2-4h IndexedDB persistence work without further disambiguation."
ranked_hypotheses:
- rank: 1
name: "TEST-INVALID — canvas-captureStream throttling in headless idle offscreen"
rationale: "Offscreen tabs are NEVER visible. Chrome throttles invisible-tab RAF + MediaRecorder. The setInterval(drawFrame, 33ms) IS in place but per code comment it's 'redundant for normal RAF but guarantees... pixel mutations every tick' — it does NOT guarantee MediaRecorder receives those mutations as frames. Over 5 min idle, MediaRecorder may have produced near-zero frames per segment. Segments .length > 0 (the empty-video throw didn't fire) but frames per segment ≈ 0 → remuxed WebM has valid headers + zero clusters → 8505 bytes."
- rank: 2
name: "CDP-ARTIFACT — worker.close() collateral teardown of offscreen"
rationale: "Puppeteer issue #9995 + upstream crbug 1371432 document the SW going into 'dead mode' under Puppeteer-induced termination. While the spike's SW respawn DID succeed (saveArchive ack), the offscreen MAY have been collateral-killed because Puppeteer CDP attach to the SW target distorts the cross-target lifecycle. Natural 30s idle eviction (not testable under Puppeteer-attach) would behave differently."
- rank: 3
name: "ARCHITECTURAL — RESEARCH MEDIUM-confidence hypothesis was wrong; offscreen DOES die with SW"
rationale: "Chrome docs explicitly state offscreen 'may outlive the service worker that created it' AND 'will be terminated if they are no longer doing work'. The active MediaRecorder + active port-keepalive should constitute 'doing work'. But the doc's language is permissive ('may'), not deterministic. Possible Chrome enforces a stricter offscreen lifetime than docs imply."
- rank: 4
name: "COMBINATION — two or more of the above interact"
rationale: "E.g., headless-throttling produces minimal frames → MediaRecorder rotation logic stalls → segments[] structure looks OK but inner blobs are tiny. The 100% determinism (4/4 runs = 8505 bytes exactly) actually argues for a SINGLE root cause, not a combination."
evidence_gaps:
- "Spike has ZERO offscreen console visibility — launch.ts filters on target.type()==='background_page' but MV3 offscreen is 'page' type"
- "No segment-count introspection during the idle window (would distinguish 'segments empty pre-kill' from 'segments lost post-kill')"
- "No comparison run against real getDisplayMedia (would isolate canvas-captureStream-throttling from architectural-loss)"
- "No comparison run against natural 30s idle eviction (would isolate CDP-artifact from architectural-loss)"
next_action: "RETURN INCONCLUSIVE verdict with routing recommendation: PAUSE plan-fix work; do NOT commit to ~2-4h IndexedDB persistence yet. Recommend low-cost disambiguation steps BEFORE plan-fix:
(a) Fix launch.ts:225 target.type filter (background_page → page) and re-run spike — captures offscreen console + reveals MediaRecorder state during idle (~30 min work);
(b) Add a get-segment-count + getSegments-size query call to the spike BEFORE the worker.close() — distinguishes 'segments empty before kill (test-invalid)' from 'segments existed before kill, lost after (architectural)' (~30 min work);
(c) IF (a)+(b) reveal segments are FULL pre-kill but EMPTY post-kill: ARCHITECTURAL hypothesis confirmed, proceed with IndexedDB plan-fix;
(d) IF (a)+(b) reveal segments are EMPTY pre-kill (frame-zero from headless throttling): the spike is test-invalid; rewrite spike with a different stream source (file-backed video or real Chrome screen capture); architectural status REMAINS OPEN but unverified."
## Symptoms
expected: "After 5 min SW idle + SAVE_ARCHIVE, the produced archive's video/last_30sec.webm should be 1-3 MB (3 × 10s segments of vp9 webm) per the offscreen-RAM segments architecture (RESEARCH Q2 MEDIUM-confidence hypothesis: offscreen survives SW idle anchored by active MediaRecorder)."
actual: "Single spike run at commit 3726eee produced videoSize=8505 bytes (corrupt WebM per ffprobe; 'End of file' + 'Duplicate element'; no valid clusters). Companion zip entries empty/lost: rrweb/session.json=[], logs/events.json=[], meta.urls=[chrome-extension://*]. SAVE_ARCHIVE returned {success:true} (SW respawned) but the offscreen buffer was lost."
errors: "ffprobe video/last_30sec.webm: 'End of file' + 'Duplicate element'; no valid clusters in 8505-byte payload"
reproduction: "HEADLESS=1 npx tsx tests/uat/spike-a33-sw-persistence.ts (~6-7 min per run). Exit code 1 = FAILED (videoSize ≤ 100KB or threw); exit code 0 = PASSED."
started: "Single observation at 2026-05-21 ~17:28 (commit 3726eee Plan 04-04 Wave 0). Architecture has been RAM-only since Plan 01-07 D-13 restart-segments; ROADMAP SC #1 verification gate was always deferred until this empirical test."
## Eliminated
(none yet — investigation just started)
## Evidence
- timestamp: "2026-05-21T17:28Z (prior spike run, committed 3726eee)"
checked: "tests/uat/spike-a33-sw-persistence.ts run #0 (the canonical Plan 04-04 Wave 0 spike)"
found: "videoSize=8505 bytes; elapsed=308.7s; corrupt WebM per ffprobe (End of file + Duplicate element); rrweb=[]; events.json=[]; meta.urls=chrome-extension://* only"
implication: "Single observation. Need 3-5 reruns to establish reproducibility before recommending multi-hour plan-fix."
- timestamp: "2026-05-21T18:15Z (debug-session research, Chrome docs)"
checked: "Chrome devrel docs: chrome.offscreen API, offscreen documents lifecycle, MV3 SW lifecycle, Puppeteer SW termination guide"
found: "(1) Per chrome.offscreen API ref: AUDIO_PLAYBACK reason has 30s auto-close after no audio; ALL OTHER REASONS (including DISPLAY_MEDIA and USER_MEDIA) have NO lifetime limit. (2) Per chromium-extensions group + chrome blog: 'Offscreen document lifetimes are not tied to the context that spawned them, meaning that an offscreen may outlive the service worker that created it. Additionally, as an ephemeral context, offscreen documents will be terminated if they are no longer doing work.' (3) Per Puppeteer issue #9995 (open since 2023-04-08, upstream crbug 1371432): 'Getting the extension service worker target through Puppeteer can cause the service worker to go into dead mode where it never wakes up.' This is a documented CDP artifact."
implication: "Chrome docs support the RESEARCH MEDIUM-confidence hypothesis (offscreen with active MediaRecorder is 'doing work' → should survive SW termination). HOWEVER, Puppeteer issue #9995 reveals a known CDP artifact where SW becomes unreachable after worker.close() — but that does NOT explain offscreen RAM loss (offscreen is a different target, not the SW). Need to determine: did worker.close() also tear down the offscreen, or did the offscreen survive but the SW respawn miscommunicate?"
- timestamp: "2026-05-21T18:18Z (source code inspection)"
checked: "src/background/index.ts:242-244 — offscreen creation parameters"
found: "chrome.offscreen.createDocument({ url, reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA], justification: 'Continuous screen recording for operator session diagnostics' })"
implication: "Reason = DISPLAY_MEDIA (correct for getDisplayMedia path). Per Chrome docs this has no explicit 30s timeout — the document SHOULD persist while MediaRecorder is active. This further supports the RESEARCH hypothesis and argues against natural-cause offscreen teardown."
- timestamp: "2026-05-21T18:21Z (spike run #1 — reproducibility check)"
checked: "HEADLESS=1 npx tsx tests/uat/spike-a33-sw-persistence.ts → /tmp/04-04-spike-run1.log"
found: "videoSize=8505 bytes (IDENTICAL to prior run #0); elapsed=308.1s; SAVE_ARCHIVE ack {success:true}; SW respawn worked"
implication: "Run #1 produced EXACTLY the same byte count as run #0. This is DETERMINISTIC, not flaky."
- timestamp: "2026-05-21T18:51Z (spike run #2)"
checked: "HEADLESS=1 npx tsx tests/uat/spike-a33-sw-persistence.ts → /tmp/04-04-spike-run2.log"
found: "videoSize=8505 bytes (IDENTICAL to runs #0, #1); elapsed=307.4s"
implication: "Run #2 confirms determinism. 3/3 observations of the exact same 8505-byte WebM. Reproducibility = 100%. (Run #3 in progress for completeness.)"
- timestamp: "2026-05-21T18:55Z (forensic webm hex dump + zip introspection)"
checked: "spike #2 zip contents + ffprobe of video/last_30sec.webm"
found: "8505 bytes = EBML header (1A 45 DF A3) + Segment header + SeekHead + Info + Tracks (V_VP9 codec) + 'webm-muxer' library identifier + LARGE void/padding region (zeros from 0x100 to ~0x2100) + closing void elements. NO Cluster elements with actual frame data. ffprobe: 'Duplicate element' + 'End of file'. rrweb/session.json contains 1 Meta event (type:4, real victim-page href). logs/events.json=[] (no user interactions during idle). meta.urls includes welcome + harness chrome-extension URLs (NOT the victim file: URL — confirms tab tracker reset)."
implication: "The remuxer DID receive segments (it produced valid WebM header + Tracks block), but the segments contained NO video frames (0 clusters). This is NOT proof of 'segments array was lost' — it could equally be proof of 'segments existed but were 0-byte/header-only'. The architecture's empty-video-buffer check at saveArchive() throws if segments.length === 0 — so segments.length > 0 by deduction. The question becomes: WHY were the segments empty of frames?"
- timestamp: "2026-05-21T19:00Z (chrome docs + offscreen-hooks code review — CONFOUNDING VARIABLE DISCOVERED)"
checked: "src/test-hooks/offscreen-hooks.ts:139-264 (installFakeDisplayMedia) + Chrome bug 653548 + chromium auto-throttled-screen-capture docs + 'Why Canvas Breaks Your Screen Recorder' blog"
found: "(1) The spike uses installFakeDisplayMedia which creates a hidden 320x180 canvas + canvas.captureStream(30) — NOT real getDisplayMedia. (2) Per the offscreen-hooks code comment itself (line 189-200): 'requestAnimationFrame fires on page-visibility heuristics in headless Chrome (offscreen documents are not visible tabs — RAF cadence drops to near-zero under certain throttling regimes, producing 0-frame segments that then crash ts-ebml decode... A 33ms setInterval (~30fps) drives drawFrame regardless of RAF throttling — redundant for normal RAF but guarantees the captureStream track sees real pixel mutations every tick.' (3) Per chromium-design docs + sendrec.eu blog: 'Chrome reducing requestAnimationFrame callbacks to roughly 1 per second in background tabs. The canvas stops updating, and the MediaRecorder records a nearly static canvas.' (4) Offscreen documents are NEVER visible tabs by design."
implication: "**CONFOUNDING VARIABLE DISCOVERED.** The 8505-byte result may NOT reflect 'offscreen RAM was lost' at all. The competing hypothesis: in headless Chrome over a 5-min idle window, the offscreen document's canvas-captureStream MediaRecorder produced very few or zero frames per segment due to either (a) headless-mode canvas/captureStream throttling NOT mitigated by the setInterval workaround, OR (b) MediaRecorder pauses/stalls when its source canvas-track sees no novel content for ~minutes. The setInterval IS in place (line 199) which should drive RAF-equivalent updates, but it's possible that during a 5-min idle the offscreen page itself was further throttled. **The spike's failure mode is consistent with BOTH 'offscreen RAM lost' AND 'canvas-captureStream throttled in headless idle' — and these have completely different remediation paths.**"
- timestamp: "2026-05-21T19:05Z (offscreen console attach analysis)"
checked: "tests/uat/lib/launch.ts:206-249 (registerOffscreenConsoleAttach)"
found: "The offscreen console listener filter on line 225 requires `target.type() === 'background_page'` — but the offscreen document target type in MV3 is `'page'` (NOT background_page; that's MV2). The grep for 'off:' in spike logs returns 0 matches across all 3 spike runs. **The offscreen console has never been observable in this spike.**"
implication: "**MAJOR OBSERVABILITY GAP.** We have zero visibility into what the offscreen document is doing during the 5-min idle — whether MediaRecorder is firing dataavailable, whether segments are rotating, whether the canvas captureStream is producing frames. The spike's evidence is purely 'video file came out 8505 bytes' which is consistent with multiple failure modes. This is exactly the situation flagged by the debugger philosophy as INCONCLUSIVE without additional observability augmentation."
## Resolution
verdict: INCONCLUSIVE
confidence: HIGH that the spike result alone CANNOT distinguish between (a) architectural-failure, (b) test-invalid headless-throttling, and (c) CDP-artifact collateral teardown.
reproducibility:
- Run #0 (2026-05-21T17:28Z, prior commit 3726eee): videoSize=8505 bytes, elapsed=308.7s
- Run #1 (2026-05-21T18:15-18:21Z, this session): videoSize=8505 bytes, elapsed=308.1s
- Run #2 (2026-05-21T18:42-18:48Z, this session): videoSize=8505 bytes, elapsed=307.4s
- Run #3 (2026-05-21T18:51-18:56Z, this session): videoSize=8505 bytes, elapsed=307.4s
- Statistics: mean=8505 bytes; range=[8505,8505]; variance=0; reproducibility=100%
- Conclusion: NOT flaky; deterministic. But determinism does NOT prove root cause — it proves the spike's failure mode is consistent. Many things would produce a deterministic 8505-byte result.
root_cause_candidates_unresolved:
- "(rank 1) Test-invalid: canvas-captureStream + headless offscreen + 5-min idle = 0-frame segments"
- "(rank 2) CDP-artifact: worker.close() collateral teardown of offscreen (Puppeteer #9995, crbug 1371432)"
- "(rank 3) Architectural: offscreen DOES die with SW despite chrome docs' permissive language"
forensic_evidence:
webm_internals: "8505 bytes = EBML header (1A 45 DF A3) + SeekHead + Info + Tracks (V_VP9 codec) + 'https://github.com/Vanilagy/webm-muxer' library marker + LARGE 0x00-padded void region (0x100 → 0x2100) + closing void elements. NO Cluster elements with frame data."
zip_contents: "video/last_30sec.webm=8505; rrweb/session.json=2 ([1 Meta event with real file:// victim URL]); logs/events.json=2 ([]); screenshot.png=33407 (success); meta.json=501 (urls=[chrome-extension://welcome, chrome-extension://harness]; NO real-page URL — tab tracker reset evidence)"
ffprobe_verdict: "'Duplicate element' (4×) at positions 73,94,115,... + 'End of file'. The 'Duplicate element' is ffprobe's signature for void-padded WebM where it tries to parse subsequent elements past the actual content."
save_archive_ack: "{success: true} — SW respawn worked; production saveArchive() ran. EmptyVideoBufferError (thrown when segments.length===0) did NOT fire — so segments.length > 0 at the time of REQUEST_BUFFER."
chrome_docs_findings:
- quote: "Offscreen document lifetimes are not tied to the context that spawned them, meaning that an offscreen may outlive the service worker that created it. As an ephemeral context, offscreen documents will be terminated if they are no longer doing work."
source: "Chrome devrel + chromium-extensions group on offscreen lifecycle"
implication: "Architecturally PERMITS the RAM-only design to work. 'May outlive' is not 'will outlive'."
- quote: "The AUDIO_PLAYBACK reason sets the document to close after 30 seconds without audio playing. All other reasons don't set lifetime limits."
source: "chrome.offscreen API reference"
implication: "DISPLAY_MEDIA has no explicit timeout. Architectural design is sound on this dimension."
- quote: "Getting the extension service worker target through Puppeteer can cause the service worker to go into dead mode where it never wakes up. ... The issue is not reproducible when the service worker inspector page is open."
source: "Puppeteer issue #9995 (open since 2023-04-08), upstream crbug 1371432"
implication: "CDP attach is KNOWN to distort SW lifecycle relative to natural eviction. The spike's worker.close() may not faithfully simulate the 'real-world 5-min idle' that ROADMAP SC #1 actually targets."
- quote: "Chrome reducing requestAnimationFrame callbacks to roughly 1 per second in background tabs. ... the MediaRecorder records a nearly static canvas."
source: "chromium auto-throttled-screen-capture design doc + sendrec.eu blog 'Why Canvas Breaks Your Screen Recorder'"
implication: "Offscreen documents are NEVER visible tabs — they are by definition 'background' from the throttling-policy POV. The spike's fake captureStream may be subject to throttling that real getDisplayMedia would not face."
- quote: "MediaRecorder using Canvas.captureStream() fails for large canvas elements on Android (Chrome Bug 897727); MediaRecorder + canvas.captureStream when [tab is backgrounded] (Chrome Bug 653548)"
source: "Chromium bug tracker"
implication: "Multiple historical Chromium bugs in this exact code path. The setInterval workaround in offscreen-hooks.ts addresses the RAF-throttling case but not necessarily the underlying captureStream-on-invisible-tab edge cases."
cdp_vs_natural_eviction_analysis:
- question: "Does worker.close() invoke self.close() via CDP ServiceWorker.unregister?"
answer: "No — worker.close() calls the CDP Target.closeTarget on the SW target (Puppeteer ≥22.1.0; per source code) which terminates the SW worker isolate. It does NOT call ServiceWorker.unregister (which would also remove the SW registration; SAVE_ARCHIVE would then never respawn the SW). The fact that SAVE_ARCHIVE respawned the SW confirms registration is intact."
- question: "Are there documented differences between CDP-induced SW termination and natural 30s idle eviction?"
answer: "Yes — natural eviction is preceded by Chrome's lifecycle hooks (e.g., the SW gets idle-timer warnings, has graceful shutdown). CDP worker.close() is abrupt — no idle-timer signal, immediate isolate termination. Per Puppeteer #9995, the abrupt path has known regressions in SW respawn behavior."
- question: "Is the offscreen's WebContents target preserved when CDP closes the SW worker target?"
answer: "UNKNOWN with the current observability. Per chromium-extensions group: 'offscreen may outlive the service worker that created it' — implies yes, in principle. But Puppeteer #9995 shows CDP can distort cross-target lifecycle in unexpected ways. WITHOUT observing the offscreen document (which we cannot — see Evidence Gap above re launch.ts:225 filter bug), we cannot verify directly."
mediarecorder_behavior_during_idle:
- context: "Offscreen page is invisible by API design (chrome.offscreen.createDocument creates an invisible document)."
- context: "MediaRecorder source = canvas.captureStream(30) where canvas is appended to offscreen DOM (offset to -9999px)."
- context: "Canvas redraws via requestAnimationFrame + setInterval(drawFrame, 33ms) (belt-and-suspenders)."
- hypothesis_A: "If RAF + setInterval both keep firing during the 5-min idle, MediaRecorder rotation produces 3 healthy ~10s segments (each 1-3 MB of real WebM data). Then worker.close() kills SW; offscreen survives per Chrome docs; SAVE_ARCHIVE respawns SW; getVideoBufferFromOffscreen returns the 3 fat segments; remux → 1-3 MB WebM. SPIKE WOULD HAVE PASSED."
- hypothesis_B: "If canvas/setInterval throttling kicks in during the idle (more aggressive than the 5-min wall-clock budget allows), MediaRecorder rotation produces 3 EMPTY (0-frame) segments. Then worker.close() doesn't matter (segments were empty BEFORE the kill). SAVE_ARCHIVE returns 3 empty segments; remux produces a header-only WebM = 8505 bytes. SPIKE FAILS but for a TEST-INVALID reason."
- hypothesis_C: "If worker.close() collaterally damages the offscreen (Puppeteer #9995 territory; cross-target side effect), MediaRecorder is torn down; segments[] (module-level RAM) is destroyed; offscreen re-created on SAVE message has empty segments[]; remux produces header-only WebM = 8505 bytes. SPIKE FAILS but for a CDP-ARTIFACT reason, not a real-world architectural failure."
conclusion: "Hypotheses B and C both predict EXACTLY the same 8505-byte result as hypothesis A (the architectural failure). The spike cannot disambiguate. The 100% deterministic 8505-byte result is necessary but not sufficient evidence for the architectural hypothesis."
routing_recommendation:
primary: "DO NOT commit to ~2-4h IndexedDB persistence plan-fix yet. The Plan 04-04 SUMMARY's framing ('SPIKE FAILED → architectural plan-fix needed') is technically correct ('the spike did fail') but the inferred root cause is unverified by the spike alone."
next_step_options:
- option: "A (cheap, ~30 min): Fix the observability gap"
action: "Edit tests/uat/lib/launch.ts:225 to use target.type()==='page' (or add 'page' alongside 'background_page'); re-run the spike; capture offscreen console; observe what MediaRecorder does during the 5-min idle. If offscreen logs show 'segment rotation OK, segments.length=3, segment[0]=1.2MB, segment[1]=1.4MB, segment[2]=1.1MB' BEFORE the worker.close(), then ARCHITECTURAL hypothesis is confirmed AND test-invalid is ruled out. If logs show 'segment rotation OK but segment[N].size ≈ 10 KB each', then test-invalid (frame-rate throttling) is confirmed."
cost: 30 min
value: "Disambiguates test-invalid from architectural. Single biggest gain per minute spent."
- option: "B (cheap, ~30 min): Add segment-count introspection to spike"
action: "Add a query call before worker.close() that calls __mokoshOffscreenQuery('get-segment-count') and __mokoshOffscreenQuery('get-segments-byte-size') (need to add the latter op to test-hooks/offscreen-hooks.ts). Log results. Disambiguates pre-kill state."
cost: 30 min
value: "Concrete pre-kill snapshot. Combined with option A, fully diagnoses."
- option: "C (cheap, ~15 min): Run spike WITHOUT worker.close()"
action: "Comment out the stopServiceWorker call in spike-a33-sw-persistence.ts; run; check videoSize. If videoSize > 100KB → the worker.close() IS the cause (architectural-or-CDP-artifact). If videoSize still ≤ 100KB → the test-invalid hypothesis is confirmed; the 5-min idle alone (no SW kill) breaks the recording."
cost: 15 min
value: "Isolates the worker.close()'s contribution to the failure."
- option: "D (expensive, ~2-4h): Skip disambiguation, commit to IndexedDB plan-fix"
action: "Original Plan 04-04 SUMMARY recommendation. Move segments from RAM to IndexedDB in offscreen. Re-run spike to verify."
cost: 2-4h implementation + 1-2h verification
value: "Closes ROADMAP SC #1 if the architectural hypothesis is correct. RISK: if test-invalid is the actual cause, this plan-fix will NOT close SC #1 (because the spike will STILL fail with 8505 bytes), but the architectural change ships anyway — adding maintenance cost + I/O failure modes (per Plan 04-04 PLAN.md threat model T-04-04 sweep) for zero closure value."
recommendation: "Options A + B + C in sequence (total ~75 min) BEFORE committing to option D. The cost is ≤5% of D's budget and the diagnostic value is high. If A+B+C jointly confirm architectural failure, D's risk is gone and IndexedDB work proceeds with full confidence. If A+B+C jointly refute architectural failure, the project saves 2-4h of work AND ROADMAP SC #1 status is reframed from 'OPEN — architecture broken' to 'OPEN — verification gate needs different test methodology'."
files_changed: []