Plan 05 closes: src/background/index.ts is now a pure coordinator with zero video-buffer state, T-1-04 mitigations on both onConnect and onMessage, OFFSCREEN_READY handshake, port-based buffer fetch via 'video-keepalive' port, IDB orphan cleanup on install, and chrome.offscreen.hasDocument() re-sync on SW respawn (audit P1 #8). 9/9 vitest tests still green; tsc clean; no as any / @ts-ignore. REQ-video-ring-buffer stays pending — Plan 07's ffprobe gate owns the final completion marker.
19 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-stabilize-video-pipeline | 05 | service-worker-coordinator |
|
|
|
|
|
|
|
|
8min | 2026-05-15 |
Phase 01 Plan 05: SW Shrink + Port Host Summary
Shrunk src/background/index.ts to a pure coordinator: deleted legacy buffer / alarms / IndexedDB / tabCapture code paths (Task 1) and wired the SW-side counterparty of the long-lived 'video-keepalive' port (Task 2) — with T-1-04 sender-id guards on both onConnect and onMessage, an OFFSCREEN_READY handshake, chrome.offscreen.hasDocument() re-sync on SW respawn, and an idempotent indexedDB.deleteDatabase('VideoRecorderDB') cleanup on install.
Performance
- Duration: ~8 min
- Started: 2026-05-15T15:53:30Z (immediately after Plan 04 completed)
- Completed: 2026-05-15T16:03:00Z
- Tasks: 2 (DELETE pass + ADD pass)
- Commits: 2 (one per task)
- Files modified: 1 (
src/background/index.ts)
Before / After Line Count
| Snapshot | Lines | Notes |
|---|---|---|
| Pre-Plan 05 (post-Plan 03 inline cleanup) | 436 | Plan 03's executor had already removed VIDEO_CHUNK / VIDEO_CHUNK_SAVED / openIndexedDB / loadChunkFromIndexedDB inline as Rule-3 blocking-fix deps. |
| Post-Task 1 (DELETE pass) | 387 | -49 lines: deletions of videoBuffer, setupKeepalive, chrome.tabCapture.getMediaStreamId, checkPermissions, requestPermissions, USER_MEDIA cast, two as any casts. |
| Post-Task 2 (ADD pass) | 491 | +104 lines: onConnect handler, getVideoBufferFromOffscreen, offscreenReady Promise, OFFSCREEN_READY case, sender-id guards, indexedDB.deleteDatabase, hasDocument check, comments documenting each mitigation. |
The plan estimated 380-440 lines post-Task 2. The actual count is 491 because (a) I added the orchestrator-requested chrome.offscreen.hasDocument() block (~15 lines including comments) and (b) I expanded the comments around each security mitigation to make T-1-04 / T-1-NEW-05-01 / T-1-NEW-05-02 / audit P1 #8 / audit P1 #12 references explicit for future auditors. The structural shrink (zero buffer state, zero alarms, zero IDB plumbing, zero tabCapture) is intact — the line-count overshoot is documentation, not code.
Task 1: Deletions
Commit: 886376e refactor(01-05): delete legacy SW buffer, alarms, IndexedDB, tabCapture paths
| Deleted Symbol / Path | Reason |
|---|---|
let videoBuffer: VideoChunk[] = [] |
D-16 — buffer ownership moved to offscreen |
function setupKeepalive + chrome.alarms.create('keepalive', ...) + chrome.alarms.onAlarm.addListener |
D-18 / audit P1 #8 — alarms never reset SW idle timer; port does |
Call site setupKeepalive() inside initialize |
Paired with the function delete |
chrome.tabCapture.getMediaStreamId({...}) block inside startVideoCapture (and its as any cast) |
D-01 — getDisplayMedia now runs inside the offscreen document |
async function checkPermissions |
Referenced 'tabCapture' (removed from manifest by D-A6); under getDisplayMedia no runtime perm check is meaningful |
async function requestPermissions |
Same reason. The popup user-gesture flows directly into startVideoCapture now |
reasons: ['USER_MEDIA'] as any in createDocument |
Replaced by canonical [chrome.offscreen.Reason.DISPLAY_MEDIA] (D-02; @types/chrome 0.0.268 exposes the enum) |
(error as any).message?.includes(...) in createDocument catch |
Replaced by error instanceof Error ? error.message : String(error) (CLAUDE.md no-as any rule) |
as any cast on chrome.tabs.sendMessage(...) response |
Replaced by an explicit `{ events?: unknown[]; userEvents?: unknown[] } |
Justification string 'Need to record video from tab for error reporting' |
Replaced with 'Continuous screen recording for operator session diagnostics' to match the new capture semantics (D-04 — NOT silent) |
Note (already done by Plan 03): addVideoChunkFromBlob, cleanupVideoBuffer, firstChunkSaved, VIDEO_BUFFER_DURATION_MS, the VIDEO_CHUNK and VIDEO_CHUNK_SAVED case branches, loadChunkFromIndexedDB, openIndexedDB, and the chrome.tabs.onActivated handler were ALREADY removed by Plan 03's Rule-3 inline cleanup (logged in STATE.md decisions). Plan 05 verified these via grep gates and removed only the residual placeholder comments.
Task 2: Additions
Commit: 5cd1519 feat(01-05): wire SW-side port host and port-based buffer fetch
onMessage switch — final shape
chrome.runtime.onMessage.addListener((message: Message, sender, sendResponse) => {
// T-1-NEW-05-01 mitigation: only accept onMessage traffic from this extension
if (sender.id !== chrome.runtime.id) {
logger.warn('Rejecting message with mismatched sender:', sender.id);
return false;
}
logger.log('Received message:', message.type, message);
switch (message.type) {
case 'REQUEST_PERMISSIONS': // → startVideoCapture(), respond {granted: true|false}
case 'GET_VIDEO_BUFFER': // → getVideoBufferFromOffscreen() → sendResponse(resp)
case 'SAVE_ARCHIVE': // → saveArchive() → sendResponse(result)
case 'OFFSCREEN_READY': // → offscreenReadyResolve?.(); offscreenReadyResolve = null
default: // → logger.warn('Unknown message type:', ...), return false
}
});
chrome.offscreen.createDocument — as committed
await chrome.offscreen.createDocument({
url: url,
reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA],
justification: 'Continuous screen recording for operator session diagnostics'
});
(Plans 06 / 07 can grep against chrome.offscreen.Reason.DISPLAY_MEDIA to confirm D-02 wiring.)
onConnect — port host
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'video-keepalive') return;
if (port.sender?.id !== chrome.runtime.id) { // T-1-04
port.disconnect();
return;
}
videoPort = port;
port.onDisconnect.addListener(() => { videoPort = null; });
});
getVideoBufferFromOffscreen — port-based RPC
async function getVideoBufferFromOffscreen(): Promise<VideoBufferResponse> {
if (videoPort === null) return { chunks: [] };
const port = videoPort;
return new Promise<VideoBufferResponse>((resolve) => {
const timer = setTimeout(() => {
port.onMessage.removeListener(handler);
resolve({ chunks: [] }); // 2 s timeout fallback
}, 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' });
});
}
onInstalled — orphan IDB cleanup
chrome.runtime.onInstalled.addListener((details) => {
// T-1-NEW-05-02 / RESEARCH.md Runtime State Inventory
try {
indexedDB.deleteDatabase('VideoRecorderDB');
} catch (e) {
logger.warn('IDB cleanup failed:', e);
}
initialize();
});
initialize() — hasDocument re-sync (P1 #8)
async function initialize() {
// After SW respawn, offscreenCreated resets to false but the offscreen
// document may still exist. Ask Chrome the ground truth.
try {
if (typeof chrome.offscreen?.hasDocument === 'function') {
const exists = await chrome.offscreen.hasDocument();
if (exists) offscreenCreated = true;
}
} catch (err) { logger.warn(...); }
}
Verification
tsc + vitest (both green)
$ npx tsc --noEmit
(exit 0, no output)
$ npx vitest run
Test Files 4 passed (4)
Tests 9 passed (9)
Start at 18:03:02
Duration 319ms
All 9 tests passing across 4 files in tests/offscreen/ — Plan 04's offscreen-side stayed untouched, so port + handshake + ring-buffer + codec-check tests all green as expected.
Grep gates (all pass)
| Gate | Expected | Actual |
|---|---|---|
chrome.alarms in src/background/ |
0 | 0 |
VideoRecorderDB / openIndexedDB / loadChunkFromIndexedDB in src/ (excluding cleanup call) |
0 outside cleanup | 2 (both the cleanup call + its log message) — only the cleanup path remains |
setupKeepalive / addVideoChunkFromBlob / cleanupVideoBuffer / tabCapture / getMediaStreamId in src/background/ |
0 | 0 |
chrome.tabs.onActivated / chrome.tabs.onUpdated in src/background/ |
0 | 0 (D-14 / D-15 satisfied) |
chrome.runtime.onConnect.addListener in src/background/index.ts |
1 | 1 |
'video-keepalive' in src/background/index.ts |
≥1 | 1 |
port.sender?.id !== chrome.runtime.id (T-1-04) |
1 | 1 |
sender.id !== chrome.runtime.id (T-1-NEW-05-01) |
1 | 1 |
indexedDB.deleteDatabase('VideoRecorderDB') |
1 | 1 |
chrome.offscreen.hasDocument (audit P1 #8) |
≥1 | 1 (the typeof + call) |
function getVideoBufferFromOffscreen |
1 | 1 |
OFFSCREEN_READY mentions |
≥1 | 3 (Promise comment + case label + log message) |
offscreenReady mentions |
≥2 | 6 (Promise var + resolve closure + await + case label + log + comment) |
as any in src/background/ |
0 | 0 |
@ts-ignore in src/background/ |
0 | 0 |
Deviations from Plan
Rule 1 — Bug fixes (auto-applied)
1. [Rule 1 - Bug] Deleted broken checkPermissions / requestPermissions flow
- Found during: Task 1.
- Issue:
chrome.permissions.contains({ permissions: ['tabCapture'] })references a permission that was removed frommanifest.jsonby D-A6 (replaced withdesktopCapture). The check would always returnfalse, sendingREQUEST_PERMISSIONSinto the never-granted branch, which itself callschrome.permissions.request({ permissions: ['tabCapture'] })— same problem. Recording could not start cleanly. - Fix: Deleted both functions entirely.
REQUEST_PERMISSIONSnow just callsstartVideoCapture()(which goes through ensureOffscreen → DISPLAY_MEDIA reason → offscreen → getDisplayMedia picker → user gesture). - Files modified:
src/background/index.ts. - Commit:
886376e.
2. [Rule 1 - Bug] Replaced two (error as any).message patterns with error instanceof Error
- Found during: Task 1 (the
as anygrep gate was at 2 instead of 0 because of a pre-existing instance in the ensureOffscreen catch). - Issue: Audit P1 #13 —
as anyviolates CLAUDE.md "noas any" rule. The catch block inensureOffscreenaccessed.messagevia(error as any).message. - Fix:
const msg = error instanceof Error ? error.message : String(error); if (msg.includes(...)) { ... }. - Files modified:
src/background/index.ts. - Commit:
886376e.
3. [Rule 1 - Bug] Replaced chrome.tabs.sendMessage(...) as any with an explicit response type
- Found during: Task 1 (same
as anygrep gate). - Issue: Same audit P1 #13 / CLAUDE.md violation; the response was typed as
anyto access.eventsand.userEvents. - Fix: Explicit
{ events?: unknown[]; userEvents?: unknown[] } | undefinedannotation on the response variable; nullish coalescing for the two extracts. - Files modified:
src/background/index.ts. - Commit:
886376e.
Rule 2 — Missing critical functionality (auto-applied)
4. [Rule 2 - Robustness] Added chrome.offscreen.hasDocument() check inside initialize()
- Found during: Task 2 (orchestrator-flagged audit P1 #8).
- Issue: Across SW respawns, the in-memory
offscreenCreatedflag resets tofalse, but the offscreen document may still be alive (it survives SW idle unload because it holds the DISPLAY_MEDIA-reason capture). The nextensureOffscreen()would then callcreateDocumentover an existing one. The catch block handles "already exists" so it's not strictly broken — but the hasDocument check makes it idempotent and is the canonical Chrome MV3 pattern (RESEARCH.md A7). - Fix:
initialize()is nowasyncand callsawait chrome.offscreen.hasDocument()to setoffscreenCreated = trueif a document is already there. Guarded withtypeof chrome.offscreen?.hasDocument === 'function'so it stays safe across @types/chrome versions and partial stubs. - Files modified:
src/background/index.ts. - Commit:
5cd1519.
Plan / orchestrator reconciliation note
The plan's must_haves and Task 1 instructions referenced symbols (addVideoChunkFromBlob, cleanupVideoBuffer, IDB helpers, VIDEO_CHUNK case, etc.) that Plan 03's executor had already inline-deleted as a Rule-3 blocking-fix dependency (documented in STATE.md). Plan 05 verified those via grep gates and only had to handle the residual videoBuffer array, the keepalive function, the tabCapture call site, and the comments. This matches the orchestrator's pre-flight note in the prompt.
TDD Gate Compliance
This plan was type: execute (not type: tdd), so the RED/GREEN/REFACTOR gate sequence does not apply. The test suite remained at 9/9 throughout — Plan 04's port + handshake tests stayed green because Plan 05 only touches the SW side, and the offscreen-side port contract is unchanged.
Authentication Gates
None — this plan is pure refactor + integration plumbing.
Known Stubs
None. All paths in the modified file have a real implementation. getVideoBufferFromOffscreen returns { chunks: [] } when videoPort === null, which is an intentional fallback (offscreen not yet connected, e.g. during SW cold start before the offscreen has finished bootstrapping). This is the documented contract per <interfaces> step 4 of the plan, not a stub.
Self-Check
Verified after writing this summary:
- ✓
886376eexists in git log (Task 1 commit). - ✓
5cd1519exists in git log (Task 2 commit). - ✓
src/background/index.tsexists (491 lines). - ✓
npx tsc --noEmitexits 0. - ✓
npx vitest runreports 9/9 PASS across 4 test files. - ✓ All Task 1 deletion grep gates return 0.
- ✓ All Task 2 addition grep gates return their expected counts.
- ✓
grep -c "as any"andgrep -c "@ts-ignore"insrc/background/both return 0.