Plan 02 RED tests for OFFSCREEN_READY handshake + port reconnect contracts; Plan 03 stub bootstrap + PortMessage types in shared/types.ts
Full long-lived port lifecycle in src/offscreen/recorder.ts — connect → ping (25 s) → REQUEST_BUFFER handler → pre-emptive reconnect (290 s) → onDisconnect synchronous reconnect
OFFSCREEN_READY handshake confirmed to fire exactly once after listener registration (Pattern 4 ordering)
T-1-04 mitigation: explicit message-shape switch in onPortMessage (defense-in-depth); sender.id === chrome.runtime.id contract documented for Plan 05's SW-side onConnect listener
01-05-sw-shrink
01-06-vite-config
01-07-ffprobe-gate
added
patterns
Long-lived port keepalive with synchronous reconnect-on-disconnect (Pitfall 4 mitigation against Chrome's ~5 min port lifetime cap)
Idempotent connectPort(): teardown timers → fresh connect → re-attach all listeners. Reentrant via onDisconnect → connectPort()
Defense-in-depth on inbound port messages: typeof-check, type-switch on known PortMessageType union, silent drop of unknown shapes
created
modified
src/offscreen/recorder.ts
Kept Plan 03's defensive bootstrap() guard (typeof chrome / per-API checks) so the pure ring-buffer + codec-check tests can import the module without a full chrome stub. Plan 04's verbatim block in the PLAN.md assumed chrome was always present — applying it as-is regressed ring-buffer + codec-check (Rule 1).
REFACTOR pass: trimmed three stale 'Plan 04 wires this' comments and replaced them with the actual D-17 / Pattern 5 citations now that Plan 04 has landed.
T-1-04 sender-id check is documented in 4 places in recorder.ts but the SW-side enforcement is Plan 05's responsibility (the offscreen INITIATES the port; the SW is the trusting party that must validate sender).
connectPort() is the single re-entry point for the port lifecycle. teardownPortTimers() runs first, fresh connect second, listeners third, timers fourth. The onDisconnect handler calls connectPort() recursively — synchronous reconnect path the Plan 02 test pins.
PORT_PING_MS = 25_000 keeps SW idle timer alive (< 30 s threshold). PORT_RECONNECT_MS = 290_000 pre-empts the ~5 min port lifetime cap before Chrome closes it on us.
4min
2026-05-15
Phase 01 Plan 04: Port Keepalive + Handshake TDD GREEN Summary
Long-lived chrome.runtime.connect port keepalive with 25 s PING loop, 290 s pre-emptive reconnect, synchronous reconnect-on-disconnect, and REQUEST_BUFFER → BUFFER response handler — flipped Plan 02 port reconnect test from RED to GREEN.
GREEN gate cleared:tests/offscreen/port.test.ts > reconnects when port disconnects now passes. All 4 test files in tests/offscreen/ are GREEN (9 tests total: 4 ring-buffer + 2 codec-check + 1 handshake + 2 port).
Full port lifecycle:connectPort() opens the port, registers onMessage (REQUEST_BUFFER → BUFFER) and onDisconnect (synchronous reconnect), schedules a 25 s PING postMessage interval, and arms a 290 s pre-emptive reconnect timer. Idempotent: reentry via onDisconnect tears down all timers cleanly before fresh connect.
D-17 contract fulfilled at the offscreen side: long-lived port now serves as the SW keepalive (replacing the deleted chrome.alarms from D-18).
T-1-04 defense-in-depth in place:onPortMessage does an explicit typeof message === 'object' + type-switch before destructuring. Any unknown port message shape is silently dropped. The SW-side sender.id === chrome.runtime.id enforcement contract is documented inline at 4 locations in recorder.ts for Plan 05's executor.
Strict-type hygiene preserved: zero as any, zero @ts-ignore, zero fallback codec strings. npx tsc --noEmit exits 0.
TDD Gate Sequence
Gate
Command
Result
RED (pre-implementation)
npx vitest run tests/offscreen/handshake.test.ts tests/offscreen/port.test.ts
1 failed / 2 passed (3 tests) — reconnects when port disconnects FAILS as expected. Captured to /tmp/01-04-red.log.
GREEN (post-implementation)
npx vitest run (full suite)
exit 0, 9/9 PASS across 4 test files. Captured to /tmp/01-04-green.log.
REFACTOR
comment cleanup in recorder.ts (header + PORT_NAME + keepalivePort)
9/9 still PASS, tsc --noEmit clean — committed as refactor(01-04): ...
RED log excerpt
FAIL tests/offscreen/port.test.ts > port reconnect > reconnects when port disconnects
AssertionError: expected 1 to be greater than or equal to 2
❯ tests/offscreen/port.test.ts:87:26
86| disconnectListeners.forEach((fn) => fn());
87| expect(connectCount).toBeGreaterThanOrEqual(2);
| ^
Test Files 1 failed | 1 passed (2)
Tests 1 failed | 2 passed (3)
GREEN test output (9/9 PASS)
RUN v4.1.6 /home/parf/projects/work/repremium
Test Files 4 passed (4)
Tests 9 passed (9)
Start at 17:47:46
Duration 294ms
Test names now all passing:
ring buffer > first chunk is header
ring buffer > second chunk is NOT header
ring buffer > trim 30s — keeps header, evicts aged tail
ring buffer > trim with empty buffer does not throw
codec strict mode > throws on unsupported vp9 and emits RECORDING_ERROR
codec strict mode > does not throw when vp9 IS supported
OFFSCREEN_READY handshake > sends OFFSCREEN_READY after listener registration
port reconnect > connects on module load
port reconnect > reconnects when port disconnects ← flipped to GREEN by Plan 04
Task Commits
#
Task
Type
Commit
1
RED-verify (no commit per plan — verify-only)
—
—
2
GREEN — wire offscreen port keepalive + OFFSCREEN_READY handshake
Final metadata commit will follow after STATE.md / ROADMAP.md updates.
Final src/offscreen/recorder.ts Shape
Total lines: 270 (was 215 after Plan 03 → +55 lines net from Plan 04)
Bootstrap structure:bootstrap() defensive-guards each chrome.runtime sub-API, registers onMessage listener, calls connectPort(), sends OFFSCREEN_READY. Order matches Pattern 4 (listener-then-ready) so the SW can safely send START_RECORDING the moment OFFSCREEN_READY arrives.
Port lifecycle exports (none — all internal): connectPort, onPortMessage, teardownPortTimers, plus the inline onDisconnect handler. Plan 05 grep-tests against the wire contract (PORT_NAME = 'video-keepalive', PortMessage types from shared/types.ts), not against this module's internal symbols.
Plan Acceptance Grep Gates (all PASS)
Gate
Expected
Actual
function connectPort
1
1 ✓
PORT_PING_MS = 25_000
1
1 ✓
PORT_RECONNECT_MS = 290_000
1
1 ✓
REQUEST_BUFFER
≥1
1 ✓
'BUFFER'
≥1
1 ✓
isFromOwnExtension
≥1
2 ✓ (definition + call)
as any
0
0 ✓
@ts-ignore
0
0 ✓
void keepalivePort
0
0 ✓ (no-unused-locals workaround removed)
T-1-04 or sender.id === chrome.runtime.id
≥1
4 ✓
chrome.runtime.connect
≥1
2 ✓
onDisconnect
≥1
2 ✓
Threat Mitigations Verified
T-1-04 (port hijack from another extension): Offscreen INITIATES the port, so the offscreen is the trusting party and the SW (Plan 05) is the listening / validating party. Plan 04 lays down: (a) an explicit typeof message === 'object' check before any destructure inside onPortMessage; (b) a type-switch on inbound PortMessageType that silently drops any unknown shape; (c) inline-comment documentation at 4 locations of recorder.ts flagging the SW-side sender.id === chrome.runtime.id requirement for Plan 05.
T-1-NEW-04-01 (port reconnect storm): The reconnect path is idempotent: teardownPortTimers() clears interval + timeout, then chrome.runtime.connect() opens a fresh port wrapped in a try / catch so a transient connect failure becomes a logged warning instead of a thrown exception. The 290 s pre-emptive timer continues to retry on schedule even if a single reconnect attempt failed.
Decisions Made
Kept Plan 03's defensive bootstrap guard. Plan 04's verbatim PLAN.md replacement assumed chrome.runtime.{onMessage,connect,sendMessage} were always present and called them unconditionally at top-level. Applying that as written regressed tests/offscreen/ring-buffer.test.ts (no chrome stub at all) and tests/offscreen/codec-check.test.ts (only sendMessage stubbed). Plan 04's <acceptance_criteria> explicitly requires all 4 test files to remain GREEN, so the fix was to wrap the bootstrap in the same bootstrap() function Plan 03 used, keeping the per-API existence checks. Production behavior is unchanged (Chrome always populates the full surface). See Deviations §1 (Rule 1).
REFACTOR pass NOT skipped. Three stale // Plan 04 ... comments on PORT_NAME, keepalivePort, and the module header were now misleading (they pointed forward to work that has landed). Replaced them with the actual citations (D-17 / Pattern 5 / Pattern 4). This satisfies the plan's "Comments that became stale after the bootstrap refactor" target. Plan 03's REFACTOR was skipped; Plan 04's is a meaningful (if small) tidy.
T-1-04 SW-side contract documented redundantly. The offscreen-side mitigation is defense-in-depth only; the actual enforcement lives in Plan 05. Documenting in 4 places (module header comment, port-name constant, threat-mitigation comment near bootstrap(), and inline at connectPort()) creates redundant signals so Plan 05's executor cannot miss the requirement when grepping for T-1-04.
Found during: Task 2 — first GREEN run after applying the plan's verbatim block as written
Issue: Plan 04's verbatim replacement (lines 219-304 of 01-04-PLAN.md) puts chrome.runtime.onMessage.addListener(...), connectPort(), and chrome.runtime.sendMessage(...) at top-level (no guard). The pure ring-buffer test (tests/offscreen/ring-buffer.test.ts) imports the module without providing any chrome stub at all, so chrome.runtime.onMessage.addListener(...) at top-level throws ReferenceError: chrome is not defined. The codec-check test stubs only chrome.runtime.sendMessage, so it throws TypeError: Cannot read properties of undefined (reading 'addListener') on the same line. Plan 04's acceptance gate explicitly requires "ALL test files in tests/offscreen/ passing (8 tests total across 4 files)" — applying the verbatim block as written fails 4 of those 9 tests (2 in ring-buffer, 2 in codec-check).
Fix: Wrapped the same bootstrap content in the bootstrap() function Plan 03 used, preserving (a) the typeof chrome === 'undefined' top-level guard and (b) the typeof chrome.runtime.X === 'function' per-API checks. Production behavior is unchanged because production always has the full chrome surface. The full port lifecycle (connectPort, onPortMessage, teardownPortTimers, ping interval, pre-emptive reconnect timer, REQUEST_BUFFER handler) is identical to the plan's verbatim block — only the outer guarding shell differs.
Files modified:src/offscreen/recorder.ts (in Task 2 commit b064a21)
Verification:npx vitest run → 9/9 PASS across 4 test files; npx tsc --noEmit exits 0; grep gates all return their expected values.
Total deviations: 1 auto-fixed (1× Rule 1 bug — plan's verbatim block was inconsistent with prior test stubs)
Impact on plan: Single localized fix; no architectural change; no scope creep. The plan's <verification><success_criteria> (all 4 test files green, T-1-04 mitigations in place, no as any / @ts-ignore) are all satisfied. The verbatim block was authored without re-checking the stubs in the existing passing tests (ring-buffer.test.ts provides no chrome stub at all; codec-check.test.ts provides only sendMessage); restoring the guard preserves both Plan 02's RED contract and Plan 04's new GREEN contract.
Issues Encountered
None beyond the Deviation §1 case above. Tests, tsc, and grep gates all behaved as the plan predicted on the second attempt (after applying the guard fix).
Threat Flags
None — no new security-relevant surface beyond what the plan's <threat_model> already enumerated (T-1-04 and T-1-NEW-04-01). T-1-04 enforcement on the SW side is Plan 05's explicit responsibility and is documented at 4 places in recorder.ts.
Plan 05 Handoff (CRITICAL — do not skip the sender check)
On SAVE_ARCHIVE: send { type: 'REQUEST_BUFFER' } over the kept-open port; resolve the saveArchive() Promise inside the port.onMessage handler when { type: 'BUFFER', chunks: [...] } arrives.
Handle SW unload window: the offscreen reconnects on disconnect; SW should NOT cache the port reference across unloads — re-bind in onConnect each time.
Delete setupKeepalive + alarms code (audit P1 #8 / D-18) — Plan 03 left it intact; Plan 05 owns its removal.
Next Phase Readiness
REQ-video-ring-buffer remains incomplete pending Plan 07 ffprobe gate (a fresh-export sample must pass ffprobe -v error -f matroska -i last_30sec.webm exit 0 — D-12). Do NOT mark the requirement complete.
Plan 05 (SW-side port host + SW shrink) can now proceed against a stable offscreen-side contract: PORT_NAME = 'video-keepalive', inbound PortMessage = REQUEST_BUFFER, outbound PortMessage = PING / BUFFER.