- Add TransferredVideoChunk { data: string (base64); type: string
(MIME); timestamp: number; isFirst?: boolean } as the
JSON-serializable shape for chunks traversing the offscreen↔SW
chrome.runtime.Port boundary.
- Retarget PortMessage.chunks?: TransferredVideoChunk[] (was
VideoChunk[]). The in-memory VideoChunk type is unchanged — both
the offscreen ring buffer and the SW-side merge step keep using
it after decoding the wire payload.
- No behavior change yet; this commit only lands the type. The
encode/decode call sites land in the next two commits.
Refs: debug session d12-blob-port-transfer-fails.
- Add blobToBase64 / base64ToBlob in src/shared/binary.ts:
portable Blob↔base64 round-trip for the chrome.runtime.Port
wire-format. JSON.stringify(blob) returns "{}" across extension
contexts, so binary payloads must travel as base64 strings.
- Mirror the GREEN-block helper signatures from
tests/offscreen/port-serialization.test.ts so the same test pins
both the standalone helpers and the production wire format.
- Land tests/offscreen/port-serialization.test.ts as the RED+GREEN
executable contract for the D-12 fix: the RED block reproduces
the 75-byte "[object Object]" failure mode byte-for-byte; the
GREEN block pins the base64 wire-format the fix must implement.
- Uses arrayBuffer() + btoa(String.fromCharCode...) rather than
FileReader: FileReader is browser-only; the chosen approach
works in both Chrome extension contexts and the Node-based
vitest environment.
Refs: debug session d12-blob-port-transfer-fails.
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>
- Delete vite.config.ts inline copy-offscreen plugin (lines 13-216):
the 174-line plugin that emitFile'd both offscreen HTML and a stringified
JS module wired to tabCapture-era chromeMediaSource + IndexedDB pipeline
(audit P0 #1 root cause; D-08 deletion target)
- Delete vite.config.ts misplaced publicDir/copyPublicDir (no public/ dir
exists; audit P2 #17) and the manualChunks=undefined shape
- Rewrite vite.config.ts to RESEARCH.md Example B form: crx() + a single
rollupOptions.input.offscreen pointing at src/offscreen/index.html
(the crxjs-managed entry Plan 03 created); 21 lines total
- Delete orphan offscreen/index.ts (audit P2 #18 dead-code, D-08)
- Delete orphan offscreen/index.html (replaced by src/offscreen/index.html
per D-07; runtime URL semantics preserved through crxjs entry binding)
- T-1-NEW-06-01 grep gate green (this.emitFile = 0)
- T-1-NEW-06-02 grep gate green (offscreen/ directory absent)
- tsc --noEmit clean; 9/9 vitest tests still green
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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 01-03-SUMMARY.md documenting RED -> GREEN gate (Plan 02 tests now
pass), 3 Rule-3 auto-fixes (OffscreenLogger inline, defensive
bootstrap, SW dead-code removal), and Plan 04 / 05 handoff notes.
- Update STATE.md: advance plan counter to 4 of 7 (43%), append
metrics + 3 execution decisions, record session.
- Update ROADMAP.md: mark Plan 01-03 [x] complete.
REQ-video-ring-buffer remains NOT complete — still pending Plans 04
(port keepalive) and 07 (ffprobe acceptance gate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
Three RED tests pin Pattern 4 (handshake) and Pattern 5 / Pitfall 4
(port reconnect on disconnect) contracts:
handshake.test.ts:
- 'sends OFFSCREEN_READY after listener registration' — exactly one
OFFSCREEN_READY emitted at module load, AFTER onMessage.addListener
port.test.ts:
- 'connects on module load' — chrome.runtime.connect called once
- 'reconnects when port disconnects' — firing onDisconnect triggers
immediate re-connect (Pitfall 4 idle-timer reset)
chrome.runtime is stubbed locally (no vitest-chrome dependency added).
No 'as any' / no '@ts-ignore'; casts are 'as unknown as T'.
Plan 04 must wire OFFSCREEN_READY send + port.connect({ name:
'video-keepalive' }) + onDisconnect-driven reconnect at the import-side
effect layer of src/offscreen/recorder.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two RED tests pin D-20 (codec strict-mode, no silent fallback):
- 'throws on unsupported vp9 and emits RECORDING_ERROR'
- 'does not throw when vp9 IS supported'
vi.resetModules() between tests is critical: module-load side-effects
(handshake + port connect) happen once per import, so isolation across
the four test files depends on it.
chrome.runtime is stubbed locally (no vitest-chrome dependency added,
per threat T-1-NEW-02-01 — minimize supply chain for four test files).
No 'as any' / no '@ts-ignore'; the cast is 'as unknown as T'.
Plan 03 must export assertCodecSupported() from src/offscreen/recorder.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four RED tests pin D-10 (header pinning) and D-11 (30s trim) contracts:
- 'first chunk is header' — isFirst marker on first addChunk
- 'second chunk is NOT header' — only the first is pinned
- 'trim 30s — keeps header, evicts aged tail' — header survives indefinitely
- 'trim with empty buffer does not throw' — defensive edge case
Plan 03 must export {addChunk, trimAged, getBuffer, resetBuffer} from
src/offscreen/recorder.ts to flip these to GREEN.
Also stages tests/fixtures/.gitkeep so the fixture dir survives clean
checkouts (Plan 07 drops a known-good last_30sec.webm into it after the
manual smoke test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add vitest@^4 to devDependencies (4.1.6 latest stable; 5.x still beta)
- Add "test": "vitest run" npm script
- Run npm install to refresh node_modules and lock file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 01-01 (Wave-0 doc cascade) complete. Six tasks landed atomically
in commits 125c032, fb88830, b1ed2cb, 597d967, 32bc996, 4a5194e. Every
code-touching plan in Phase 1 (01-02..01-07) now reads a consistent
baseline: getDisplayMedia replaces tabCapture in DEC-003; long-lived
port replaces alarms in DEC-010; manifest.json carries the final
Phase-1 permissions set (desktopCapture, activeTab, downloads,
scripting, storage, offscreen).
SUMMARY: .planning/phases/01-stabilize-video-pipeline/01-01-SUMMARY.md
Replace 'tabCapture' with 'desktopCapture' to match the new
getDisplayMedia capture path (D-A6). Remove 'alarms' because the
Phase 01 SW keepalive moves to a long-lived chrome.runtime.connect
port and the alarms code is deleted in Plan 05; declaring an unused
permission expands attack surface and is mitigated here per T-1-02.
activeTab is retained for chrome.tabs.captureVisibleTab in Phase 3,
and offscreen is retained for chrome.offscreen.createDocument.
Rewrite the Phase 1 one-liner in the Phases list to call out the
chrome.tabCapture -> getDisplayMedia swap, and rewrite Success
Criterion #2 to describe the new operator-selected screen/window
capture and the absence of tab-reattach logic. Phases 2-5 sections are
untouched. The verbatim phrase 'no tab re-attach logic' is preserved
as written in the plan to document the amendment in-place; the
original 'recorder re-attaches to the new active tab' wording is gone.
Replace the REQ-video-ring-buffer bullet to bind the new
getDisplayMedia + offscreen-document acquisition path and drop the
'active-tab' wording (the new API is screen/window-scoped, not
tab-scoped, so there is nothing to re-attach on tab switch). Encoding,
buffer window, and SPEC §10 acceptance citations are unchanged. Adds
CON-display-capture-binding alongside the existing constraint bindings.
Rewrite DEC-003 and DEC-010 rows in the Key Decisions table to reflect
the Phase 01 amendments (getDisplayMedia + long-lived port keepalive),
each citing the .planning/intel/decisions.md amendment block as the
canonical source. Swap the two Constraints bullets that cited
chrome.alarms keepalive and tabCapture binding for replacement
bullets bound to CON-display-capture-binding.
Append RETIRED blocks to CON-tab-capture-binding and
CON-service-worker-keepalive (the two SPEC-derived constraints that are
no longer valid under getDisplayMedia + port-keepalive). Add new
CON-display-capture-binding consolidating the replacement contract.
Originals stay intact for provenance; RETIRED is appended below each.
Append Amendment blocks to DEC-003 (getDisplayMedia replaces tabCapture)
and DEC-010 (long-lived port replaces alarms keepalive) so downstream
phases see the new API contract. Original text intact; amendments are
appended, not replacements. Maintains intel/* provenance for the
Phase 01 doc cascade.
After gsd-plan-phase 1: 7 plans across 7 waves. All gates pass:
- Plan-checker (sonnet) VERIFICATION PASSED on iteration 1
- Decision coverage gate (gsd-sdk): 19/19 decisions covered
- Requirements coverage: REQ-video-ring-buffer in all plans
- Security threat model: T-1-01/02/04 mitigated; T-1-03 accepted residual
Known non-blocking gaps:
- gsd-sdk roadmap.annotate-dependencies failed (t.trim is not a function);
ROADMAP plan-list annotations skipped. Phase 1 plan-list in ROADMAP.md
remains accurate; this is a cosmetic nice-to-have for cross-cutting
constraint visibility.
- 1 plan-checker warning (stale wave prose in Plan 03/04 objectives) was
fixed during decision-coverage revision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames "## Open Questions" header to "## Open Questions (RESOLVED)" and
adds inline RESOLVED markers to each of the three questions:
- Q1 (MediaRecorder timeslice cluster alignment) → D-12 ffprobe gate
(Plan 03 Task 2 + Plan 07 Task 1) + D-13 fallback (pre-staged skeleton
in src/offscreen/recorder.ts per Plan 03)
- Q2 (5-minute port lifetime cap) → Plan 04's 290 s pre-emptive reconnect
plus synchronous onDisconnect → connectPort reconnect path
- Q3 (crxjs path-emit behavior) → Plan 06 Task 2 runtime verification +
conditional src/background/index.ts edit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes:
1. wave: 3 → 6 (cascade: max(wave(05)=4, wave(06)=5)+1 = 6).
2. Task 1 <automated> verify now prefixes the ffprobe invocation with
test -f tests/fixtures/last_30sec.webm && which ffprobe so the gate
fails fast with a clear signal if the human checkpoint never produced
the fixture (instead of ffprobe blowing up with a cryptic file-not-found).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three changes to resolve the blocker:
1. depends_on: ["03"] → ["03", "05"] — Plan 06 Task 2 conditionally edits
src/background/index.ts which Plan 05 writes; the original wave 2
collocation with Plan 05 was a same-wave file conflict.
2. wave: 2 → 5 (cascade: max(wave(03)=2, wave(05)=4)+1 = 5).
3. files_modified gains src/background/index.ts (Task 2 path-adjustment
edit is now declared in frontmatter so the executor sees the contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 05 depends_on: ["03", "04"], so wave must be max(2, 3)+1 = 4, not 2
(cascade from Plan 04 wave change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes:
1. wave: 1 → 2 (cascade after Plan 02 wave fix)
2. OffscreenLogger: ...args: any[] → ...args: unknown[] for strict-mode
hygiene. Existing Logger / ContentLogger are left on the legacy any[]
pattern (refactoring is out of Phase 1 scope) — divergence documented
via style_divergence_note frontmatter field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents that all 6 tasks are doc-text-only edits with no TypeScript
compilation — cognitive load is substantially lower than code plans of
equal task count. Avoids fragmenting the atomic doc-cascade by splitting
into 01-01a / 01-01b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Researched Chrome MV3 offscreen + DISPLAY_MEDIA, MediaRecorder cluster
alignment, SW port keepalive, crxjs offscreen entry, ffprobe verification.
Identified the D-12/D-13 fallback hinge: timeslice=2000ms does NOT force
keyframe alignment (Chrome kf_max_dist=100); Pattern 2 (age-trim) may need
to escalate to Pattern 3 (restart-segments) if ffprobe rejects.
Architecture verified against two in-the-wild production extensions
(Proscreen-S3, meeting_mate) using the exact CONTEXT.md D-01..D-05 path.
The OFFSCREEN_READY handshake (audit P1 #12) and long-lived port keepalive
(audit P1 #8) are wired together. .planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings chosen for the Mokosh stabilization work:
- model_profile: quality (Opus everywhere except verification)
- git.branching_strategy: phase (gsd/phase-{N}-{slug} per phase)
- workflow.code_review: true with depth=deep (cross-file analysis with
import graphs — appropriate given the SW ↔ offscreen ↔ content-script
messaging interplay)
- workflow.tdd_mode: true (RED/GREEN/REFACTOR gates for eligible tasks,
retrofits regression coverage as we touch each file)
All other settings inherit gsd-sdk defaults (research/plan-check/verifier
on, balanced commit_docs, no Intel/Graphify indexing yet).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preserves the explicit-manifest input used to bootstrap .planning/ so the
ingest is reproducible. Two docs were classified: Тз расширение фаза1.md
(SPEC, precedence 0) and README.md (DOC, precedence 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>