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.
- Update module header to list port keepalive + OFFSCREEN_READY among
the module's owned responsibilities (no longer "wired by Plan 04")
- Replace 'Plan 04 owns the ping loop' on PORT_NAME with the actual
D-17 + Pattern 5 citation
- Replace 'Plan 04 fills the lifecycle' on keepalivePort with its
D-17 + Pattern 5 role
Pure comment cleanup — no behavior change. All 9 tests still GREEN.
- Add PORT_PING_MS (25s) and PORT_RECONNECT_MS (290s) constants
- Replace stub bootstrap with full long-lived port lifecycle:
- connectPort() registers onMessage/onDisconnect listeners, schedules
25s PING postMessages and a 290s pre-emptive reconnect (Pitfall 4
belt-and-braces against Chrome's ~5min port lifetime cap)
- onDisconnect handler synchronously calls connectPort() again
(Plan 02 port.test.ts pins this; flips reconnect test to GREEN)
- REQUEST_BUFFER over the port responds with { type: 'BUFFER',
chunks: getBuffer() } (Plan 05 SW-side will issue REQUEST_BUFFER
on export)
- Keep defensive guard on chrome.runtime sub-APIs so pure ring-buffer
and codec-check tests can import the module without a full chrome stub
- Remove the no-longer-needed 'void keepalivePort' workaround (the
variable is now used by onPortMessage + connectPort)
- T-1-04 mitigation: explicit message-shape switch in onPortMessage
(any unknown port message type silently dropped); comment block
documents the SW-side sender.id check contract for Plan 05
GREEN: all 4 test files in tests/offscreen/ pass (9 tests total —
ring-buffer 4 + codec-check 2 + handshake 1 + port 2).
npx tsc --noEmit exits 0. Zero 'as any' / '@ts-ignore' in recorder.ts.
- 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>
- Add src/offscreen/recorder.ts (214 lines) — Phase 01 source of truth
owning getDisplayMedia capture, MediaRecorder lifecycle, in-memory ring
buffer with WebM header retention + 30 s age trim, codec strict-mode
(D-20), and track.onended cleanup.
- Add src/offscreen/index.html — crxjs-managed bundle entry referencing
./recorder.ts.
- Add OffscreenLogger class to src/shared/logger.ts (uses ...args:
unknown[] for strict-mode hygiene; legacy Logger / ContentLogger keep
...args: any[] per project provenance). Bundled into this commit
because recorder.ts cannot typecheck without the import (Rule 3 —
blocking dependency).
- Pre-stage D-13 restart-segments fallback as commented skeleton at
bottom of recorder.ts so Plan 07's fallback path needs no re-plan.
- Defensive bootstrap (typeof chrome guard) so the pure ring-buffer +
codec tests can import the module without stubbing the full chrome
surface (Rule 3 — Plan 02 ring-buffer test does not stub chrome).
Flips Plan 02's RED tests to GREEN:
- tests/offscreen/ring-buffer.test.ts — 4 tests passing
- tests/offscreen/codec-check.test.ts — 2 tests passing
Handshake test also passes (single OFFSCREEN_READY emission); port
reconnect test stays RED until Plan 04 wires the reconnect loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>