461 lines
24 KiB
Markdown
461 lines
24 KiB
Markdown
---
|
||
phase: 01-stabilize-video-pipeline
|
||
plan: 05
|
||
type: execute
|
||
wave: 4
|
||
depends_on: ["03", "04"]
|
||
files_modified:
|
||
- src/background/index.ts
|
||
autonomous: true
|
||
requirements:
|
||
- REQ-video-ring-buffer
|
||
requirements_addressed:
|
||
- REQ-video-ring-buffer
|
||
|
||
must_haves:
|
||
truths:
|
||
- "`src/background/index.ts` no longer contains `addVideoChunkFromBlob`, `cleanupVideoBuffer`, `setupKeepalive`, `loadChunkFromIndexedDB`, `openIndexedDB`, or any `chrome.alarms` reference — buffer ownership moves to offscreen per D-16/D-19, and the alarms-driven keepalive is DELETED per D-18 (it never actually reset the SW idle timer; the long-lived port does)"
|
||
- "`src/background/index.ts` no longer calls `chrome.tabCapture.getMediaStreamId` (D-01 amendment); video acquisition is now `getDisplayMedia` invoked from the offscreen module"
|
||
- "`src/background/index.ts` no longer handles `VIDEO_CHUNK` or `VIDEO_CHUNK_SAVED` (deleted Message types in Plan 03)"
|
||
- "`src/background/index.ts` no longer contains any `chrome.tabs.onActivated` handler tied to the recording lifecycle (D-14: tab-switch re-attach is non-applicable under `getDisplayMedia`; D-15: operator tab-switching no longer interrupts recording, the buffer keeps filling regardless of active tab)"
|
||
- "SW has an `onConnect` listener that filters `port.name === 'video-keepalive'` and validates `port.sender?.id === chrome.runtime.id` (T-1-04 mitigation; this is the SW-side counterparty of the long-lived port keepalive per D-17)"
|
||
- "SW has an `onMessage` `OFFSCREEN_READY` case that resolves a pending readiness Promise (Pattern 4 SW side)"
|
||
- "SW's `SAVE_ARCHIVE` and `GET_VIDEO_BUFFER` handlers fetch the buffer via the port (`REQUEST_BUFFER` → wait for `BUFFER`) instead of holding their own `videoBuffer` array"
|
||
- "SW's `ensureOffscreen` uses `chrome.offscreen.Reason.DISPLAY_MEDIA` (not `USER_MEDIA`) per D-02"
|
||
- "SW's `onInstalled` listener calls `indexedDB.deleteDatabase('VideoRecorderDB')` once as a cleanup pass (RESEARCH.md Runtime State Inventory)"
|
||
- "`npx tsc --noEmit` exits 0"
|
||
artifacts:
|
||
- path: "src/background/index.ts"
|
||
provides: "Shrunk SW coordinator: lifecycle + port host + export buffer-fetch only; no buffer state, no alarms, no IndexedDB"
|
||
contains: "video-keepalive"
|
||
key_links:
|
||
- from: "src/background/index.ts (onConnect)"
|
||
to: "src/offscreen/recorder.ts (connectPort)"
|
||
via: "shared port name 'video-keepalive'"
|
||
pattern: "video-keepalive"
|
||
- from: "src/background/index.ts (SAVE_ARCHIVE handler)"
|
||
to: "src/background/index.ts (getVideoBufferFromOffscreen)"
|
||
via: "port REQUEST_BUFFER round-trip"
|
||
pattern: "REQUEST_BUFFER"
|
||
- from: "src/background/index.ts (ensureOffscreen)"
|
||
to: "src/offscreen/index.html"
|
||
via: "chrome.offscreen.createDocument url"
|
||
pattern: "src/offscreen/index.html"
|
||
---
|
||
|
||
<objective>
|
||
Shrink `src/background/index.ts` to its new responsibilities: offscreen
|
||
lifecycle, port host, and export-time buffer fetch. DELETE the SW-side
|
||
ring-buffer state and helpers (now owned by offscreen per D-16), DELETE the
|
||
chrome.alarms keepalive (D-18), DELETE the IndexedDB code path (D-19),
|
||
DELETE the `chrome.tabCapture.getMediaStreamId` call (D-01 amendment),
|
||
DELETE the `VIDEO_CHUNK` / `VIDEO_CHUNK_SAVED` message handlers (their
|
||
message types were removed in Plan 03), DELETE any `chrome.tabs.onActivated`
|
||
re-attach plumbing (D-14: not applicable under the new capture API; D-15:
|
||
operator tab switches no longer interrupt the recording), and WIRE the
|
||
SW-side `onConnect` handler against the `'video-keepalive'` port that
|
||
Plan 04 opens from the offscreen.
|
||
|
||
Purpose: REQ-video-ring-buffer's data flow on export is `popup →
|
||
SAVE_ARCHIVE → SW → REQUEST_BUFFER (via port) → offscreen → BUFFER (via
|
||
port) → SW → assemble ZIP → download`. The SW NEVER holds a chunk locally —
|
||
which is the only way the buffer survives SW idle unloads (D-16).
|
||
|
||
Output: A heavily-shrunk `src/background/index.ts` that compiles cleanly,
|
||
holds zero video-buffer state, talks to the offscreen exclusively via
|
||
runtime messaging + the long-lived port, and tolerates the IndexedDB
|
||
remnant by deleting it on first install.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
|
||
@.planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md
|
||
@.planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md
|
||
@src/background/index.ts
|
||
@src/shared/types.ts
|
||
@src/offscreen/recorder.ts
|
||
</context>
|
||
|
||
<interfaces>
|
||
Port contract (already implemented in offscreen by Plan 04):
|
||
- Port name: `'video-keepalive'`
|
||
- Offscreen → SW outbound: `{ type: 'PING' }` every 25 s (informational; SW just receives it as keepalive traffic)
|
||
- Offscreen → SW outbound on REQUEST_BUFFER: `{ type: 'BUFFER', chunks: VideoChunk[] }`
|
||
- SW → offscreen inbound: `{ type: 'REQUEST_BUFFER' }`
|
||
|
||
SW side must:
|
||
1. Register `chrome.runtime.onConnect.addListener` that filters by port name AND validates sender ID
|
||
2. Store the connected port in a module-level `let videoPort: chrome.runtime.Port | null`
|
||
3. Clear the reference on disconnect (offscreen will reconnect; SW gets a new onConnect call)
|
||
4. Expose `getVideoBufferFromOffscreen(): Promise<VideoBufferResponse>` that:
|
||
- Returns `{chunks: []}` early if no port is connected
|
||
- Otherwise sends `{type: 'REQUEST_BUFFER'}` over the port
|
||
- Resolves when a `{type: 'BUFFER', chunks: ...}` reply arrives
|
||
- Times out at 2 s with `{chunks: []}`
|
||
|
||
Existing SW behaviors to PRESERVE:
|
||
- `mergeVideoChunks(chunks: VideoChunk[]): Blob` (unchanged)
|
||
- `createArchive(...)` (unchanged signature; calls `mergeVideoChunks` and zips with JSZip)
|
||
- `downloadArchive(blob)` (unchanged)
|
||
- `captureScreenshot()` (unchanged — Phase 3 owns popup-side rework)
|
||
- `chrome.runtime.onInstalled` listener (existing, gets an indexedDB cleanup line added)
|
||
- `onMessage` cases: `REQUEST_PERMISSIONS`, `GET_VIDEO_BUFFER`, `SAVE_ARCHIVE` — KEPT but their bodies change to talk to offscreen via port
|
||
|
||
Removed message types (from Plan 03's edit to src/shared/types.ts):
|
||
- `'VIDEO_CHUNK'` — handler block deleted
|
||
- `'VIDEO_CHUNK_SAVED'` — handler block deleted
|
||
|
||
`chrome.offscreen.Reason.DISPLAY_MEDIA`: try `reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA]` first. If the current `@types/chrome` (0.0.268) does NOT expose `DISPLAY_MEDIA` and `tsc --noEmit` fails, fall back to a narrowing cast (no `as any`): `reasons: ['DISPLAY_MEDIA' as chrome.offscreen.Reason]`. The executor verifies which form compiles by running `npx tsc --noEmit` after the edit.
|
||
</interfaces>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| extension contexts → SW `onConnect` | Any extension context can attempt to open a port to the SW; only the offscreen has a legitimate reason for the `'video-keepalive'` port |
|
||
| popup / content script → SW `onMessage` | Existing trust boundary; this plan adds T-1-04 sender-id check on the SW-side onMessage too |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|-------------|-----------------|
|
||
| T-1-04 | Spoofing — port-hijack on SW side | `chrome.runtime.onConnect` handler | mitigate | The SW `onConnect` listener filters `port.name === 'video-keepalive'` AND `port.sender?.id === chrome.runtime.id`. A non-extension caller cannot open a runtime port at all (Chrome enforces); the sender-id check is defense-in-depth for the within-extension case. Grep gate: `grep -v '^#' src/background/index.ts \| grep -c "port.sender?.id !== chrome.runtime.id"` returns at least 1. |
|
||
| T-1-NEW-05-01 | Information Disclosure — buffer pulled by unauthorized message | `onMessage` handlers (`SAVE_ARCHIVE`, `GET_VIDEO_BUFFER`) | mitigate | The SW-side `onMessage` listener already exists; this plan adds a `sender.id === chrome.runtime.id` guard at the top of the listener (per RESEARCH.md Security Domain table). |
|
||
| T-1-NEW-05-02 | Tampering — stale IndexedDB orphan | `indexedDB.deleteDatabase('VideoRecorderDB')` in `onInstalled` | mitigate | After this phase deletes the inline plugin's IndexedDB code, browser profiles that previously ran the old build still have a `VideoRecorderDB` database. The SW `onInstalled` listener calls `indexedDB.deleteDatabase('VideoRecorderDB')` idempotently to clear it. The call is harmless if the DB never existed. Grep gate: `grep -c "indexedDB.deleteDatabase('VideoRecorderDB')" src/background/index.ts` returns 1. |
|
||
</threat_model>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto">
|
||
<name>Task 1: DELETE — drop legacy buffer + alarms + IndexedDB + tabCapture paths from SW</name>
|
||
<files>src/background/index.ts</files>
|
||
<read_first>
|
||
- src/background/index.ts (the full current file: 536 lines)
|
||
- .planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md §`src/background/index.ts` (lines 276-396) — the verified delete-target table
|
||
- .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md §"Files to DELETE in this phase"
|
||
</read_first>
|
||
<action>
|
||
The current `src/background/index.ts` has been verified at 536 lines. Apply the following deletions using the Edit tool. Re-verify each line range with `grep -n` BEFORE editing (line numbers below are from the on-disk state captured 2026-05-15; do NOT trust them blindly — re-run grep first):
|
||
|
||
(1) Delete `addVideoChunkFromBlob` function — currently lines 26-45 inclusive (the entire `// Кольцевой буфер видео` block through the closing `}`). Also remove the preceding `// Кольцевой буфер видео` comment.
|
||
|
||
(2) Delete `cleanupVideoBuffer` function — currently lines 47-75 inclusive.
|
||
|
||
(3) Delete `firstChunkSaved` from module state — line 17 (`let firstChunkSaved = false; // Флаг что первый чанк уже сохранен`).
|
||
|
||
(4) Delete `videoBuffer` from module state — line 16 (`let videoBuffer: VideoChunk[] = [];`).
|
||
|
||
(5) Replace the `chrome.tabCapture` call inside `startVideoCapture` — currently lines 126-144. The replacement is VERBATIM:
|
||
|
||
```typescript
|
||
// Создаём offscreen документ (с reason DISPLAY_MEDIA per D-02)
|
||
await ensureOffscreen();
|
||
|
||
// Просим offscreen запустить запись — getDisplayMedia вызывается там
|
||
// (D-01: больше нет chrome.tabCapture.getMediaStreamId).
|
||
logger.log('Sending START_RECORDING to offscreen...');
|
||
try {
|
||
await chrome.runtime.sendMessage({
|
||
type: 'START_RECORDING'
|
||
});
|
||
logger.log('START_RECORDING sent successfully');
|
||
} catch (msgError) {
|
||
logger.error('Failed to send START_RECORDING:', msgError);
|
||
throw msgError;
|
||
}
|
||
```
|
||
|
||
The existing `const [tab] = ...` line at the top of `startVideoCapture` and the `if (!tab.id || !tab.url) throw ...` check can be retained — they don't harm anything. Phase 1 keeps them.
|
||
|
||
(6) Replace `ensureOffscreen` reason — currently line 90 reads `reasons: ['USER_MEDIA'] as any,`. Replace VERBATIM:
|
||
|
||
```typescript
|
||
reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA],
|
||
```
|
||
|
||
If `chrome.offscreen.Reason.DISPLAY_MEDIA` is NOT in the current `@types/chrome` (0.0.268) and `tsc --noEmit` fails, fall back to (no `as any`):
|
||
|
||
```typescript
|
||
reasons: ['DISPLAY_MEDIA' as chrome.offscreen.Reason],
|
||
```
|
||
|
||
(7) Update the justification text on line 91 to match RESEARCH.md Example C:
|
||
|
||
```typescript
|
||
justification: 'Continuous screen recording for operator session diagnostics'
|
||
```
|
||
|
||
(8) Delete `setupKeepalive` function — currently lines 156-165.
|
||
|
||
(9) Delete the `setupKeepalive()` call inside `initialize` — currently line 525.
|
||
|
||
(10) Delete the `VIDEO_CHUNK` case from the onMessage switch — currently lines 457-466.
|
||
|
||
(11) Delete the `VIDEO_CHUNK_SAVED` case — currently lines 468-473.
|
||
|
||
(12) Delete `loadChunkFromIndexedDB` function — currently lines 482-505.
|
||
|
||
(13) Delete `openIndexedDB` function — currently lines 507-520.
|
||
|
||
(14) Verify no `chrome.tabs.onActivated` listener exists in the file (D-14 / D-15: tab-switch handling is non-applicable under the new capture API). Run `grep -n "chrome.tabs.onActivated" src/background/index.ts`. If the grep returns any hits, DELETE those lines (the entire listener callback block). If the grep returns nothing, log "D-14/D-15 satisfied: no tab-switch handler found in SW" in the task summary.
|
||
|
||
After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChunk` is reported as unused after the deletes, that indicates a function that needs it was inadvertently lost; STOP and audit.
|
||
</action>
|
||
<verify>
|
||
<automated>npx tsc --noEmit && [ $(grep -cE "addVideoChunkFromBlob|cleanupVideoBuffer|setupKeepalive|loadChunkFromIndexedDB|openIndexedDB|getMediaStreamId|chrome\.alarms|chrome\.tabs\.onActivated" src/background/index.ts) -eq 0 ]</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `npx tsc --noEmit` exits 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "addVideoChunkFromBlob"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "cleanupVideoBuffer"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "setupKeepalive"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "loadChunkFromIndexedDB"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "openIndexedDB"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "getMediaStreamId"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "VIDEO_CHUNK_SAVED"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "chrome.alarms"` returns 0
|
||
- `grep -v '^#' src/background/index.ts | grep -c "chrome.tabs.onActivated"` returns 0 (D-14/D-15 mitigation)
|
||
- `grep -c "DISPLAY_MEDIA" src/background/index.ts` returns 1
|
||
- `grep -c "as any" src/background/index.ts` returns 0 (CLAUDE.md rule)
|
||
- File line count reduced from 536 to roughly 380-400 lines (allow ±40)
|
||
</acceptance_criteria>
|
||
<done>SW shed every legacy path. tsc clean. File ~30% smaller. Ready for the port-host wiring in Task 2.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2: ADD — wire SW-side port host, sender check, onInstalled IndexedDB cleanup, and port-based buffer fetch</name>
|
||
<files>src/background/index.ts</files>
|
||
<read_first>
|
||
- src/background/index.ts (post-Task-1 state)
|
||
- src/offscreen/recorder.ts (read-only reference for the offscreen-side port contract)
|
||
- .planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md §"Pattern 5: SW-side port handling" (lines 590-628)
|
||
- .planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md §`src/background/index.ts` ADD blocks (lines 372-396)
|
||
</read_first>
|
||
<action>
|
||
Seven targeted additions using the Edit tool (NOT a full rewrite).
|
||
|
||
(1) **Module-level state additions** — after the existing `let cachedScreenshot: Blob | null = null;` line, ADD VERBATIM:
|
||
|
||
```typescript
|
||
// Port from offscreen (D-17). Re-assigned on every (re)connect.
|
||
let videoPort: chrome.runtime.Port | null = null;
|
||
// Offscreen readiness Promise — set up at module load, resolved on first
|
||
// OFFSCREEN_READY message (Pattern 4). startVideoCapture awaits this before
|
||
// sending START_RECORDING, so we never lose the popup's transient activation
|
||
// to a race with the offscreen bootstrap.
|
||
let offscreenReadyResolve: (() => void) | null = null;
|
||
const offscreenReady: Promise<void> = new Promise((res) => {
|
||
offscreenReadyResolve = res;
|
||
});
|
||
```
|
||
|
||
(2) **onConnect handler** — add VERBATIM AFTER the `ensureOffscreen` function block. Place it BEFORE `startVideoCapture` so the listener is registered before any code might trigger a port open:
|
||
|
||
```typescript
|
||
// SW-side port host (D-17, RESEARCH.md Pattern 5). The offscreen opens this
|
||
// port on bootstrap and reconnects on disconnect. We use it for: (a)
|
||
// keepalive traffic (PING) — Chrome 110+ resets the SW idle timer on every
|
||
// port message; (b) on-demand REQUEST_BUFFER round-trip during SAVE_ARCHIVE.
|
||
chrome.runtime.onConnect.addListener((port) => {
|
||
// T-1-04 mitigation: only accept ports from this extension
|
||
if (port.name !== 'video-keepalive') {
|
||
return;
|
||
}
|
||
if (port.sender?.id !== chrome.runtime.id) {
|
||
logger.warn('Rejecting port with mismatched sender:', port.sender?.id);
|
||
port.disconnect();
|
||
return;
|
||
}
|
||
logger.log('Offscreen port connected');
|
||
videoPort = port;
|
||
port.onDisconnect.addListener(() => {
|
||
logger.log('Offscreen port disconnected; offscreen will reconnect');
|
||
videoPort = null;
|
||
});
|
||
// Inbound traffic is mostly PING (ignored) and BUFFER (handled by the
|
||
// per-request listener installed in getVideoBufferFromOffscreen).
|
||
});
|
||
```
|
||
|
||
(3) **Buffer-fetch function** — add VERBATIM AFTER the onConnect block above:
|
||
|
||
```typescript
|
||
const BUFFER_FETCH_TIMEOUT_MS = 2_000;
|
||
|
||
async function getVideoBufferFromOffscreen(): Promise<VideoBufferResponse> {
|
||
if (videoPort === null) {
|
||
logger.warn('No offscreen port available; returning empty buffer');
|
||
return { chunks: [] };
|
||
}
|
||
const port = videoPort;
|
||
return new Promise<VideoBufferResponse>((resolve) => {
|
||
const timer = setTimeout(() => {
|
||
port.onMessage.removeListener(handler);
|
||
logger.warn(`Buffer fetch timed out after ${BUFFER_FETCH_TIMEOUT_MS} ms`);
|
||
resolve({ chunks: [] });
|
||
}, BUFFER_FETCH_TIMEOUT_MS);
|
||
const handler = (msg: unknown) => {
|
||
if (
|
||
typeof msg === 'object' &&
|
||
msg !== null &&
|
||
(msg as { type?: unknown }).type === 'BUFFER'
|
||
) {
|
||
clearTimeout(timer);
|
||
port.onMessage.removeListener(handler);
|
||
const chunks = (msg as { chunks?: VideoChunk[] }).chunks ?? [];
|
||
resolve({ chunks });
|
||
}
|
||
};
|
||
port.onMessage.addListener(handler);
|
||
port.postMessage({ type: 'REQUEST_BUFFER' });
|
||
});
|
||
}
|
||
```
|
||
|
||
(4) **Delete `getVideoBuffer` (the synchronous version that returned the local array)** — find and delete the function currently around lines ~168-172 post-deletes (`function getVideoBuffer(): VideoBufferResponse { return { chunks: videoBuffer }; }`). It no longer compiles after Task 1 deletes `videoBuffer`.
|
||
|
||
(5) **Replace the `GET_VIDEO_BUFFER` case** — in the onMessage switch, find `case 'GET_VIDEO_BUFFER':` and replace its body VERBATIM:
|
||
|
||
```typescript
|
||
case 'GET_VIDEO_BUFFER':
|
||
getVideoBufferFromOffscreen().then((resp) => sendResponse(resp));
|
||
return true;
|
||
```
|
||
|
||
(6) **Update `saveArchive` to use the port-based fetch** — find the `const videoBuffer = getVideoBuffer();` line (currently around line 332 in the post-Task-1 file). Replace VERBATIM:
|
||
|
||
```typescript
|
||
const videoBufferResp = await getVideoBufferFromOffscreen();
|
||
logger.log(`Video buffer: ${videoBufferResp.chunks.length} chunks`);
|
||
```
|
||
|
||
And update the subsequent `createArchive(videoBuffer, ...)` call to use `videoBufferResp` instead. Specifically:
|
||
|
||
```typescript
|
||
const archiveBlob = await createArchive(
|
||
videoBufferResp,
|
||
rrwebEvents,
|
||
userEvents,
|
||
screenshot
|
||
);
|
||
```
|
||
|
||
(7) **Add OFFSCREEN_READY case + await in startVideoCapture** — two sub-edits:
|
||
|
||
(7a) Add a new case to the onMessage switch, placed AFTER `case 'SAVE_ARCHIVE':` and BEFORE the `default:` block. VERBATIM:
|
||
|
||
```typescript
|
||
case 'OFFSCREEN_READY':
|
||
logger.log('OFFSCREEN_READY received');
|
||
offscreenReadyResolve?.();
|
||
offscreenReadyResolve = null;
|
||
return false;
|
||
```
|
||
|
||
(7b) In `startVideoCapture`, ADD `await offscreenReady;` AFTER `await ensureOffscreen();`. The relevant excerpt becomes:
|
||
|
||
```typescript
|
||
await ensureOffscreen();
|
||
await offscreenReady;
|
||
logger.log('Sending START_RECORDING to offscreen...');
|
||
// ... rest unchanged
|
||
```
|
||
|
||
(8) **Sender check on onMessage** — at the very top of the existing `chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {` callback, rename `_sender` to `sender` and add a guard. Replace the listener header VERBATIM:
|
||
|
||
```typescript
|
||
chrome.runtime.onMessage.addListener((message: Message, sender, sendResponse) => {
|
||
if (sender.id !== chrome.runtime.id) {
|
||
logger.warn('Rejecting message with mismatched sender:', sender.id);
|
||
return false;
|
||
}
|
||
logger.log('Received message:', message.type, message);
|
||
```
|
||
|
||
(9) **onInstalled IndexedDB cleanup** — find the existing `chrome.runtime.onInstalled.addListener` block and REPLACE entirely VERBATIM:
|
||
|
||
```typescript
|
||
chrome.runtime.onInstalled.addListener((details) => {
|
||
logger.log('Extension installed/updated:', details.reason);
|
||
// RESEARCH.md Runtime State Inventory — clean up orphaned IndexedDB from
|
||
// pre-Phase-01 builds. Idempotent: no-op if DB never existed.
|
||
try {
|
||
indexedDB.deleteDatabase('VideoRecorderDB');
|
||
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
|
||
} catch (e) {
|
||
logger.warn('indexedDB.deleteDatabase failed:', e);
|
||
}
|
||
initialize();
|
||
});
|
||
```
|
||
|
||
After ALL these edits, run:
|
||
|
||
```bash
|
||
npx tsc --noEmit
|
||
npx vitest run
|
||
```
|
||
|
||
Both MUST exit 0.
|
||
</action>
|
||
<verify>
|
||
<automated>npx tsc --noEmit && npx vitest run && grep -c "video-keepalive" src/background/index.ts && grep -c "VideoRecorderDB" src/background/index.ts</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `npx tsc --noEmit` exits 0
|
||
- `npx vitest run` exits 0 (all 8 offscreen tests still pass — Plan 05 only touches SW, but the offscreen tests should not regress)
|
||
- `grep -c "chrome.runtime.onConnect.addListener" src/background/index.ts` returns 1
|
||
- `grep -c "'video-keepalive'" src/background/index.ts` returns at least 1
|
||
- `grep -c "port.sender?.id !== chrome.runtime.id" src/background/index.ts` returns 1 (T-1-04 mitigation)
|
||
- `grep -c "sender.id !== chrome.runtime.id" src/background/index.ts` returns 1 (onMessage sender check)
|
||
- `grep -c "indexedDB.deleteDatabase('VideoRecorderDB')" src/background/index.ts` returns 1
|
||
- `grep -c "function getVideoBufferFromOffscreen" src/background/index.ts` returns 1
|
||
- `grep -c "REQUEST_BUFFER" src/background/index.ts` returns at least 1
|
||
- `grep -c "offscreenReady" src/background/index.ts` returns at least 2 (the Promise variable + the await)
|
||
- `grep -c "OFFSCREEN_READY" src/background/index.ts` returns at least 1 (the case label)
|
||
- `grep -c "as any" src/background/index.ts` returns 0
|
||
- `grep -c "@ts-ignore" src/background/index.ts` returns 0
|
||
</acceptance_criteria>
|
||
<done>SW side fully wired against the offscreen's port. Sender checks in place. IndexedDB cleanup landed on onInstalled. The SW is now a pure coordinator — it holds no buffer state of its own.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
After both tasks land:
|
||
|
||
1. `npx tsc --noEmit` — exits 0.
|
||
2. `npx vitest run` — exits 0 (8 tests passing across 4 files in tests/offscreen/).
|
||
3. The grep gates listed in Task 2's `acceptance_criteria` all return their expected values.
|
||
4. `wc -l src/background/index.ts` — line count between 380 and 440 (the file shrunk from 536 by ~100-150 lines as the legacy paths went away and ~30-40 lines were added for the port host).
|
||
|
||
Commit cadence: TWO commits.
|
||
- Task 1: ONE commit (`refactor(01-05): delete legacy SW buffer + alarms + IndexedDB + tabCapture paths`).
|
||
- Task 2: ONE commit (`feat(01-05): wire SW-side port host and port-based buffer fetch`).
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- `src/background/index.ts` carries no buffer state, no alarms, no IndexedDB plumbing, no `tabCapture` calls, no `chrome.tabs.onActivated` re-attach plumbing
|
||
- SW has `onConnect` handler matching the offscreen's port (Plan 04 counterparty)
|
||
- SW has `OFFSCREEN_READY` handshake handler resolving a readiness Promise
|
||
- T-1-04 mitigations in place on BOTH onConnect (sender + port name) and onMessage (sender)
|
||
- IndexedDB orphan cleanup runs on onInstalled
|
||
- `tsc --noEmit` clean; no `as any`, no `@ts-ignore`
|
||
</success_criteria>
|
||
|
||
<output>
|
||
After completion, create `.planning/phases/01-stabilize-video-pipeline/01-05-SUMMARY.md` with:
|
||
- Before/after line count for src/background/index.ts (was 536, now N)
|
||
- List of every deleted symbol from Task 1 (so future audits can grep)
|
||
- The final shape of the onMessage switch (which cases survived)
|
||
- A snippet showing the exact `chrome.offscreen.createDocument` call as committed (so Plan 06 / 07 can grep against it)
|
||
- Confirmation that `npx vitest run` shows all 8 tests passing (port + handshake stay green because Plan 04's offscreen-side stayed unchanged)
|
||
- Two commit SHAs
|
||
</output>
|