Files
mokosh/.planning/debug/sw-offscreen-persistence-investigation-session-2.md
Mark 4ea1bbb7a8 docs(debug): SC#1 sw-offscreen-persistence investigation session 2 — REFUTED-architecture (canvas-captureStream issue)
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>
2026-05-22 08:02:24 +02:00

97 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.