Session-2 (continuation of d614462 INCONCLUSIVE) executed disambiguation
plan and converged on a definitive verdict. Three independent observations
ruled out ALL architectural-failure hypotheses:
Step A: race-tolerant offscreen target attach (committed separately;
enabled visibility into the offscreen recorder + remux pipeline).
Step B: pre-kill and post-kill segment-count probes via the existing
`__mokoshOffscreenQuery 'get-segment-count'` bridge op (no new
test-only symbols introduced; FORBIDDEN_HOOK_STRINGS inventory
unchanged at 12 entries). Observed segments.length transition:
POST-PRIME=0 → PRE-KILL=3 → POST-KILL=3
Segments structurally survive the SW kill (offscreen still responds
to bridge query post-kill). Hypothesis A (architectural RAM loss
across SW termination) REFUTED.
Step C: SPIKE_SKIP_SW_KILL=1 env-var mode skips worker.close(). The
resulting videoSize is IDENTICAL to the canonical run (8505 bytes).
Hypothesis C (CDP-induced offscreen collateral teardown) REFUTED.
Since SW was not killed, its console listener stayed connected,
exposing the full Remux pipeline output:
[SW:Remux] Segment ts=1: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Segment ts=2: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Segment ts=3: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Remux complete: 0 frames, total timeline=0ms, output=8505 bytes
Each segment Blob has a valid track header (PixelWidth/Height parsed
successfully) but ZERO VP9 frames. Hypothesis B (canvas-captureStream
throttling in headless idle) CONFIRMED.
VERDICT: REFUTED-architecture (canvas-captureStream issue).
The architecture (offscreen-RAM `segments: Blob[] = []`) works
correctly; the spike's test methodology is invalid. The
`installFakeDisplayMedia` synthetic stream (canvas.captureStream(30)
on a hidden -9999px-offset 320x180 canvas) cannot sustain frame
production during a 5-min headless idle window despite the
`setInterval(drawFrame, 33ms)` belt-and-suspenders mitigation. This
matches the documented Chromium throttling of MediaRecorder on
invisible-canvas sources (Chrome bug 653548; auto-throttled-screen-capture
design doc; sendrec.eu blog "Why Canvas Breaks Your Screen Recorder").
ROUTING RECOMMENDATION (out of scope for this debug session):
- Do NOT proceed with the IndexedDB persistence plan-fix proposed by
Plan 04-04 SUMMARY. The plan-fix would NOT close SC #1 because the
spike would STILL produce 8505 bytes after IDB lands — the failure
is in the test's fake stream, not in segment persistence.
- Open a new plan slot (likely Plan 04-08 or a Phase 5 plan) that
reframes SC #1 verification methodology. Options:
(a) real getDisplayMedia in non-headless Puppeteer with
--auto-select-desktop-capture-source;
(b) video-file-backed MediaStream source (HTMLVideoElement
playing a bundled WebM) — bypasses canvas-captureStream
throttling entirely;
(c) reduce SC #1 wall-clock idle threshold to a value short
enough that canvas-captureStream survives (e.g., 30s) AND
add a separate manual operator-empirical test for 5-min.
ROADMAP SC #1 status: REMAINS OPEN. The architecture is sound; the
empirical verification gate is broken. Plan 04-04 SUMMARY's
characterization ("spike FAILED → architectural plan-fix needed") is
TECHNICALLY CORRECT on the first clause but INCORRECT on the second —
the spike's failure mode is in test infrastructure, not in production
code.
Files in this commit:
- tests/uat/spike-a33-sw-persistence.ts: added probeSegmentCount
helper using existing __mokoshOffscreenQuery bridge op; 3
checkpoints (POST-PRIME / PRE-KILL / POST-KILL); SPIKE_SKIP_SW_KILL=1
env-var skips worker.close() for Step C disambiguation.
- .planning/debug/sw-offscreen-persistence-investigation-session-2.md:
NEW session-2 debug note documenting full evidence trail + verdict
derivation + routing recommendation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
12 KiB
Markdown
97 lines
12 KiB
Markdown
---
|
||
status: diagnosed
|
||
trigger: "Continuation of session-1 (committed at d614462). Prior verdict INCONCLUSIVE; ~75 min cheap disambiguation plan: Step A fix offscreen target filter + re-run + capture logs; Step B segment-count probe pre-kill; Step C spike without worker.close(). Synthesize verdict for SC#1 plan-fix routing."
|
||
created: 2026-05-21T19:30:00Z
|
||
updated: 2026-05-21T20:35:00Z
|
||
parent_session: ".planning/debug/sw-offscreen-persistence-investigation.md (committed d614462)"
|
||
goal: find_root_cause_only
|
||
verdict: REFUTED-architecture (canvas-captureStream issue)
|
||
recommendation: "REFUTE the Plan 04-04 SUMMARY's framing 'spike FAILED → architectural plan-fix needed'. The architecture works correctly; the SPIKE methodology is invalid. ROADMAP SC#1 remains OPEN but the previously-recommended IndexedDB persistence plan-fix would NOT close it (the spike would STILL produce 8505 bytes after IDB persistence is added, because the failure is in the test's fake-stream pipeline, not in the persistence layer). Reframe SC#1 verification methodology BEFORE doing any architectural work."
|
||
---
|
||
|
||
## Current Focus
|
||
|
||
verdict: REFUTED-architecture (canvas-captureStream issue)
|
||
confidence: HIGH — three independent spike runs converge on the same conclusion via different observability channels.
|
||
hypothesis_state: "RESOLVED. Root cause was Hypothesis B (canvas-captureStream + headless idle = 0-frame segments). Hypotheses A (architectural RAM loss) and C (CDP-induced offscreen teardown) are REFUTED by empirical evidence below."
|
||
next_action: "Return verdict + recommendation to orchestrator. Plan-fix routing decision: do NOT proceed with IndexedDB persistence plan-fix; instead, propose a Phase 5 plan that reframes SC#1 verification methodology (real getDisplayMedia in non-headless Puppeteer; OR a video-file-backed fake stream that survives headless throttling)."
|
||
|
||
## Symptoms
|
||
|
||
(Inherited verbatim from session-1; IMMUTABLE.)
|
||
|
||
expected: "After 5 min SW idle + SAVE_ARCHIVE, video buffer survives at 1-3 MB."
|
||
actual: "videoSize=8505 bytes (deterministic; 4/4 runs in session-1 + 2/2 runs in session-2 + 1/1 Step-C variant = 7/7 reproducibility)."
|
||
errors: "ffprobe: 'End of file' + 'Duplicate element'; no valid clusters."
|
||
reproduction: "HEADLESS=1 npx tsx tests/uat/spike-a33-sw-persistence.ts (~6 min wall-clock)."
|
||
|
||
## Eliminated
|
||
|
||
- hypothesis: "(A) Architectural — offscreen-RAM `segments: Blob[] = []` lost across SW termination"
|
||
evidence: "Direct observation via the `get-segment-count` bridge probe at 3 checkpoints in the canonical spike: POST-PRIME=0, PRE-KILL=3, POST-KILL=3. Segments structurally survive the SW kill at full count. The offscreen document itself responds to the post-kill probe (segments.length=3), so it did not die with the SW. ALSO: Step C (no SW kill) produces the IDENTICAL 8505-byte result — kill is irrelevant."
|
||
timestamp: 2026-05-21T19:55:00Z
|
||
|
||
- hypothesis: "(C) CDP-artifact — Puppeteer worker.close() collaterally tears down offscreen via cross-target side effect (Puppeteer #9995 territory)"
|
||
evidence: "Step C spike WITHOUT worker.close() produces IDENTICAL 8505-byte videoSize. Removing the SW-kill step does NOT improve the outcome. The CDP attach is not implicated."
|
||
timestamp: 2026-05-21T20:01:00Z
|
||
|
||
## Evidence
|
||
|
||
- timestamp: "2026-05-21T19:30Z"
|
||
checked: "Existing `__mokoshOffscreenQuery` 'get-segment-count' bridge op at src/test-hooks/offscreen-hooks.ts:493-523"
|
||
found: "The bridge op already exists. The harness can dispatch chrome.runtime.sendMessage({type: '__mokoshOffscreenQuery', op: 'get-segment-count'}) from the harness page realm, and the offscreen-hooks's onMessage listener responds with {count: number} where count=segmentCountGetter() which is closure over recorder's module-level segments.length. This is a PRODUCTION TEST SURFACE (gated by __MOKOSH_UAT__, in the canonical 12-string FORBIDDEN_HOOK_STRINGS inventory)."
|
||
implication: "Step B uses this without adding ANY new symbol — zero impact on FORBIDDEN_HOOK_STRINGS count. Probe rides production surface."
|
||
|
||
- timestamp: "2026-05-21T19:48Z (canonical spike with probes + race-broken offscreen attach)"
|
||
checked: "HEADLESS=1 npx tsx tests/uat/spike-a33-sw-persistence.ts → /tmp/04-04-debug-step-ab.log"
|
||
found: "SPIKE PROBE [POST-PRIME]: segments.length=0; SPIKE PROBE [PRE-KILL]: segments.length=3; SPIKE PROBE [POST-KILL]: segments.length=3. Final videoSize=8505 bytes."
|
||
implication: "Segments accumulated to MAX_SEGMENTS=3 during the 5-min idle (recorder rotation worked). Segments survived the SW kill (count unchanged at 3). YET the final WebM is 8505 bytes (no frames). Conclusion: segments structurally exist but contain NO video frames — they are Blob OBJECTS with valid track metadata but empty Cluster sections. This rules out Hypothesis A (segments lost across SW termination) at the structural level."
|
||
|
||
- timestamp: "2026-05-21T20:00Z (Step C spike: SPIKE_SKIP_SW_KILL=1)"
|
||
checked: "HEADLESS=1 SPIKE_SKIP_SW_KILL=1 npx tsx tests/uat/spike-a33-sw-persistence.ts → /tmp/04-04-debug-step-c.log"
|
||
found: "Identical 8505-byte videoSize. Crucially, since SW was NOT killed, its console listener stayed connected, so we see the FULL Remux pipeline output: 'Remuxing 3 segments; sizes: [array Array]' followed by 'Segment ts=1: 0 frames, duration=0ms, trackInfo=320x180' (and identical lines for ts=2 + ts=3) and 'Remux complete: 0 frames, total timeline=0ms, output=8505 bytes'."
|
||
implication: "DEFINITIVE: each of the 3 segments has a valid track header (trackInfo=320x180 was parsed from each) but ZERO VP9 frames. The Remuxer faithfully emitted an 8505-byte WebM containing only EBML headers + Segment + Tracks + Cues (no Clusters). The 5-min headless idle window prevented MediaRecorder from receiving frame data from the canvas-captureStream source despite the offscreen-hooks setInterval(drawFrame, 33) belt-and-suspenders. Hypothesis B (canvas-captureStream throttling) is empirically confirmed; Hypothesis C (CDP-induced teardown) is empirically refuted (Step C removed the CDP-kill and the failure persisted)."
|
||
|
||
- timestamp: "2026-05-21T20:30Z (diagnostic spike — tests/uat/spike-diagnose-offscreen-target.ts)"
|
||
checked: "Puppeteer browser.targets() enumeration with active offscreen recording (post-assertA2 prime)"
|
||
found: "browser.targets() count=7 targets. Offscreen reported as: type=background_page url=chrome-extension://{id}/src/offscreen/index.html. Crucially, the targetcreated event fires for the offscreen with type='other' and url='' (metadata not yet stabilized at create time); type+url stabilize AFTER the target's first navigation completes."
|
||
implication: "ROOT CAUSE OF SESSION-1 OBSERVABILITY BUG: the launch.ts `targetcreated` listener checked target.type() and target.url() at event time, but the metadata is unstable at that instant. The original `type==='background_page'` filter was actually CORRECT for the stable enumeration — it failed only because checked at the wrong time. Fix: bind to BOTH 'targetcreated' AND 'targetchanged' (the latter fires when URL stabilizes) + enumerate `browser.targets()` as a race-free safety net. URL pattern is the load-bearing match criterion; type-based discrimination is unreliable across event timings."
|
||
|
||
- timestamp: "2026-05-21T20:31Z (verification — diagnostic with the fix in place)"
|
||
checked: "HEADLESS=1 npx tsx tests/uat/spike-diagnose-offscreen-target.ts → grep [off:"
|
||
found: "[off:log] [OS:Recorder] Recording started (D-13 restart-segments), mime: video/webm;codecs=vp9 segment_ms: 10000 max_segments: 3"
|
||
implication: "Step A fix verified — offscreen console capture now functional. The (launch: offscreen console attached — url=...) diagnostic confirms the attach happened via the targetchanged event after the URL stabilized."
|
||
|
||
## Resolution
|
||
|
||
root_cause: "Test methodology (NOT production architecture). The spike's `installFakeDisplayMedia()` synthetic stream (canvas.captureStream(30) on a hidden 320x180 canvas appended to the offscreen DOM, offset to -9999px) cannot sustain frame production during a 5-min wall-clock idle window in headless Chrome. The offscreen-hooks setInterval(drawFrame, 33ms) belt-and-suspenders is INSUFFICIENT to mitigate the documented Chromium auto-throttling of MediaRecorder on invisible-canvas sources (Chrome bug 653548, auto-throttled-screen-capture design doc, sendrec.eu blog 'Why Canvas Breaks Your Screen Recorder'). The MediaRecorder produces structurally-valid WebM segments (EBML header + Tracks block parses correctly; trackInfo=320x180 extracted) but each segment's Cluster section is empty. The Remuxer faithfully emits a header-only 8505-byte WebM."
|
||
|
||
fix: "Plan 04-04 spike's test methodology needs replacement. The architecture (offscreen-RAM segments: Blob[] = []) is sound; it correctly preserves the 3 segments across SW kill (proven by post-kill probe = 3). RECOMMENDATIONS (out of scope for this debug session): (a) reframe SC#1 verification to use real getDisplayMedia in non-headless Puppeteer with --auto-select-desktop-capture-source (currently rejected per 01-11-SUMMARY falsification 4 as 'unreliable in headless'; may be reliable in non-headless); OR (b) replace the canvas-captureStream fake with a video-file-backed source (e.g., MediaStream from an HTMLVideoElement playing a bundled WebM) which doesn't suffer the invisible-canvas throttling; OR (c) lower SC#1's wall-clock idle threshold to a value short enough that canvas-captureStream survives (e.g., 30s) AND add a separate manual operator-empirical test for the full 5-min case. The ARCHITECTURAL change (IndexedDB persistence) recommended in Plan 04-04 SUMMARY would NOT close SC#1 — the spike would STILL produce 8505 bytes after IDB lands because the failure is in the test's fake stream, not in segment persistence."
|
||
|
||
verification: "Three independent spike runs converge: (1) canonical with broken observability → 8505 bytes; pre-kill probe count=3, post-kill count=3. (2) canonical with race-fixed observability (post-Step-A) → 8505 bytes; pre-kill count=3, post-kill count=3. (3) Step C variant (skip worker.close()) → 8505 bytes; Remux output explicitly shows '0 frames' for each segment. The architecture works (segments survive); the test methodology is broken (segments are 0-frame Blobs)."
|
||
|
||
files_changed:
|
||
- "tests/uat/lib/launch.ts (Step A fix — race-tolerant offscreen target attach: bind targetcreated AND targetchanged + browser.targets() enumeration; URL-based match; updated docstrings citing empirical evidence)"
|
||
- "tests/uat/spike-a33-sw-persistence.ts (Step B — added `probeSegmentCount` helper using existing `__mokoshOffscreenQuery` 'get-segment-count' bridge op; 3 probes at POST-PRIME/PRE-KILL/POST-KILL; Step C — added SPIKE_SKIP_SW_KILL=1 mode that skips the worker.close() call)"
|
||
- "tests/uat/spike-diagnose-offscreen-target.ts (NEW — one-off diagnostic script that enumerated browser.targets() to reveal the Puppeteer event-timing bug; safe to keep committed as a future SW-lifecycle debugging tool)"
|
||
- ".planning/debug/sw-offscreen-persistence-investigation-session-2.md (NEW — this debug note)"
|
||
|
||
## Synthesis — Verdict Decision Tree
|
||
|
||
Per the disambiguation_protocol Step D:
|
||
|
||
- **CONFIRMED-architecture:** REJECTED. Segments survive SW kill; pre/post-kill counts both = 3. Removing the SW kill (Step C) does NOT improve outcome. Persistence is not the issue.
|
||
|
||
- **REFUTED-architecture (canvas-captureStream issue):** CONFIRMED.
|
||
- Pre-kill probe: count=3 → segments accumulated correctly.
|
||
- Step C (no kill) → identical 8505-byte failure.
|
||
- Direct Remux log evidence: `Segment ts=1: 0 frames, duration=0ms, trackInfo=320x180` × 3.
|
||
- Documented Chromium throttling behavior matches exactly: invisible-canvas MediaRecorder → near-zero frame production over time.
|
||
- Setinterval(drawFrame, 33) workaround empirically insufficient against the actual throttling path (canvas pixels change BUT captureStream track stops emitting frames).
|
||
|
||
- **REFUTED-architecture (CDP artifact):** REJECTED. Step C (without worker.close()) produces same failure. CDP attach is not the cause.
|
||
|
||
- **STILL-INCONCLUSIVE:** REJECTED. The three independent observations (segment count, Step-C, Remux logs) converge unambiguously on canvas-captureStream throttling.
|
||
|
||
**Routing recommendation:** Do NOT proceed with the IndexedDB persistence plan-fix proposed by Plan 04-04 SUMMARY. Open a new plan slot (likely Plan 04-08 or a Phase 5 plan) that reframes SC#1 verification methodology. Architecture is sound; verification gate is broken.
|