- Read incoming port.chunks as TransferredVideoChunk[] (was
VideoChunk[] — but that was a lie because Blob doesn't survive
JSON serialization across the port boundary).
- Decode each wire chunk via base64ToBlob(wire.data, wire.type) and
resolve VideoBufferResponse with the resulting VideoChunk[]. The
existing mergeVideoChunks downstream sees real Blobs and produces
a real WebM-prefixed merged blob.
- Defensive per-chunk decode: log + skip individual decode failures
rather than blowing up the whole fetch. Falls back to
video/webm;codecs=vp9 if the wire chunk somehow omits the type
(defense-in-depth — the offscreen always populates it).
- Document the 2 s BUFFER_FETCH_TIMEOUT_MS budget: covers worst-case
encode + post-message + JSON parse with > 1.5 s of headroom for
the current 15-chunk × 100 KB sizing.
Refs: debug session d12-blob-port-transfer-fails, D-17 port lifecycle.
After collapsing vite.config.ts to use rollupOptions.input.offscreen =
'src/offscreen/index.html', crxjs preserves the 'src/' prefix in the
bundled output (Outcome A per RESEARCH.md Pitfall 5 dichotomy):
dist/src/offscreen/index.html (NOT dist/offscreen/index.html)
The pre-amendment leftover string 'offscreen/index.html' at
src/background/index.ts:45 would have produced
ERR_FILE_NOT_FOUND in chrome.offscreen.createDocument and broken
Plan 07's manual smoke load. Updated to match the actual emit path.
- npm run build exits 0; 7 dist/assets/*.js bundles produced
- dist/manifest.json permissions: desktopCapture present, tabCapture absent
- tsc --noEmit clean; 9/9 vitest tests still green
- ensureOffscreen URL string now matches dist/src/offscreen/index.html
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 05 Task 2 — make the SW a pure coordinator that talks to the offscreen
via the long-lived 'video-keepalive' port (D-17, RESEARCH.md Pattern 5).
Additions:
- chrome.runtime.onConnect.addListener handler scoped to port name
'video-keepalive' + T-1-04 mitigation (port.sender?.id check). Stores port
in module-level videoPort: chrome.runtime.Port | null.
- getVideoBufferFromOffscreen(): port-based REQUEST_BUFFER round-trip with
a 2s timeout fallback to { chunks: [] }. Replaces the synchronous SW-local
getVideoBuffer() stub from Task 1.
- offscreenReady Promise + OFFSCREEN_READY onMessage case (RESEARCH.md
Pattern 4): startVideoCapture awaits this before sending START_RECORDING,
closing the 'Receiving end does not exist' race window (audit P1 #12).
- onMessage GET_VIDEO_BUFFER + SAVE_ARCHIVE rewritten to fetch the buffer
via the port instead of the deleted local array.
- onMessage sender.id !== chrome.runtime.id guard at handler top
(T-1-NEW-05-01 mitigation).
- chrome.runtime.onInstalled now calls indexedDB.deleteDatabase('VideoRecorderDB')
once to clean up the orphaned database from pre-Phase-01 builds
(T-1-NEW-05-02 / RESEARCH.md Runtime State Inventory).
Rule 2 deviation (orchestrator-flagged robustness):
- initialize() now calls chrome.offscreen.hasDocument() to detect existing
offscreen documents across SW respawns and update offscreenCreated
accordingly (audit P1 #8). Guarded with a typeof check to stay safe under
partial chrome stubs.
Verified: npx tsc --noEmit clean; npx vitest run 9/9 green (Plan 04
offscreen-side tests stay un-touched); no as any / @ts-ignore.
Plan 05 Task 1 — finish the SW shrink:
- DELETE videoBuffer: VideoChunk[] module state (buffer lives in offscreen per D-16)
- DELETE setupKeepalive + chrome.alarms registration (D-18; alarms never reset SW idle timer — port does)
- DELETE chrome.tabCapture.getMediaStreamId call (D-01: getDisplayMedia now runs inside offscreen)
- DELETE chrome.permissions.contains/request for tabCapture (broken — desktopCapture is the new manifest entry, but getDisplayMedia needs no runtime perm)
- DELETE comment-only references to removed symbols (so grep gates pass)
- REPLACE 'USER_MEDIA' as any → chrome.offscreen.Reason.DISPLAY_MEDIA (D-02; @types/chrome 0.0.268 exposes it)
- REPLACE justification copy to match RESEARCH.md Example C
- FIX (error as any) → instanceof Error pattern (CLAUDE.md rule)
- FIX chrome.tabs.sendMessage cast: explicit response type instead of 'as any'
- COLLAPSE REQUEST_PERMISSIONS handler: under getDisplayMedia, no runtime perm check is meaningful — just call startVideoCapture() (Rule 1 deviation; old code returned granted=false because tabCapture is no longer in manifest)
- Temporary stub: getVideoBuffer() returns { chunks: [] } — Task 2 deletes this and wires the port-based getVideoBufferFromOffscreen()
Verified: npx tsc --noEmit clean, npx vitest run 9/9 green, no as any / @ts-ignore.
- Add PortMessageType and PortMessage interface to src/shared/types.ts
for the long-lived port (offscreen ↔ SW; D-17 / Plan 04 wires the
ping loop + REQUEST_BUFFER / BUFFER traffic).
- Remove 'VIDEO_CHUNK' and 'VIDEO_CHUNK_SAVED' from MessageType union
(per D-19 — chunks no longer travel via chrome.runtime.sendMessage;
the IndexedDB SW-side plumbing is the audit P0 #2 broken path).
- OffscreenLogger class was already added alongside Task 2 because
recorder.ts imports it at module top.
Inline SW cleanup (Rule 3 — blocking dependency, plan acceptance gates
on `npx tsc --noEmit` exit 0):
- Remove src/background/index.ts VIDEO_CHUNK + VIDEO_CHUNK_SAVED case
branches (refs to deleted union members).
- Remove now-unreferenced loadChunkFromIndexedDB / openIndexedDB
(only reachable from the deleted VIDEO_CHUNK_SAVED branch).
- Remove now-unreferenced addVideoChunkFromBlob / cleanupVideoBuffer
/ firstChunkSaved / VIDEO_BUFFER_DURATION_MS constant (the SW-side
ring buffer now lives in src/offscreen/recorder.ts per D-16).
- Keep SW-side `videoBuffer: VideoChunk[] = []` as a placeholder; Plan
04 wires it to fetch from offscreen over the keepalive port. The
remaining `getVideoBuffer` + `saveArchive` callers continue to
compile against the empty array until Plan 04 lands.
- Plan 05 owns the broader SW shell cleanup.
Verification (post-commit):
- npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts → 6/6 PASS
- npx tsc --noEmit → exit 0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>