Plans cover the post-D-13 architecture and the auto-start UX charter expansion that landed during 2026-05-16 UAT: - Plan 01-08 — WebM remux via ts-ebml@3.0.2 + webm-muxer@5.1.4. Replaces the broken file-concat in mergeVideoSegments with a real single-EBML remux. Drives the 2 RED tests in tests/offscreen/webm-playback.test.ts to GREEN. Regenerates the canonical fixture against the remuxer. 5 tasks (4 TDD + 1 operator empirical checkpoint), wave 1. - Plan 01-09 — Whole-desktop constraint (displaySurface:'monitor', cursor:'always') + post-grant validation, chrome.action.onClicked direct toolbar invocation, chrome.action badge state machine (REC/OFF/ERROR), chrome.runtime.onStartup notification + recovery notification on onUserStoppedSharing, popup scoped to SAVE-only. 17 new test assertions across 4 test files. smoke.sh updated to auto-select an entire screen. 5 tasks (4 TDD + 1 operator empirical checkpoint), wave 2 (depends on 01-08). - Plan 01-10 — chrome.runtime.onInstalled welcome tab on first install via chrome.storage.local guard; vanilla welcome.html/ts/css bundle with single "Начать запись" button consuming install-time activation. Uses centralized Logger pattern. 4 tasks (3 TDD + 1 operator empirical checkpoint), wave 3 (depends on 01-09). CONTEXT.md amendment block appended with 4 disambiguated decisions: - D-14-remux: WebM remux supersedes D-13 file-concat - D-15-display-surface: whole-desktop + cursor visibility lifted from Phase 5 deferral - D-16-toolbar: toolbar onClicked + popup SAVE-only + badge state machine + onStartup/recovery notifications - D-17-onboarding: welcome tab on first install (distinct from D-17-port-lifecycle from Option C) The earlier D-17 port-lifecycle heading also renamed to hyphenated form for cross-ref consistency. Plan-check loop: 3 iterations (initial + 2 revisions). Iteration 1 surfaced 11 findings (2 BLOCKER + 6 WARNING + 3 INFO); all addressed via revision iter 1 with checker-recommended fixes. Iteration 2 surfaced 3 derivative regressions (literal-string grep anchors from the iter-1 fixes did not match live CONTEXT.md); all addressed in iter 2 with empirically-validated literals. Iteration 3 PASSED clean. Validation: gsd-sdk frontmatter.validate + verify.plan-structure both return valid=True for all 3 plans. Plan 01-08 Task 4 verify-chain grep tested end-to-end against live CONTEXT.md (exit 0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
578 lines
29 KiB
Markdown
578 lines
29 KiB
Markdown
# Phase 1: Stabilize Video Pipeline — Context
|
||
|
||
**Gathered:** 2026-05-15
|
||
**Status:** Ready for planning
|
||
|
||
<domain>
|
||
## Phase Boundary
|
||
|
||
Stabilize the video-capture pipeline so the extension continuously records the
|
||
operator's session into a ring buffer and produces a **playable** WebM on
|
||
demand. The phase ends when:
|
||
|
||
1. The video buffer is alive end-to-end with **one** authoritative source of
|
||
truth for the recorder code (no parallel offscreen implementations).
|
||
2. The MediaRecorder lifecycle is correct (no shadowed bindings, predictable
|
||
start/stop).
|
||
3. The buffer holds at most 30 s of footage at any moment AND concatenating
|
||
the retained chunks produces a file that `ffprobe -v error` accepts as
|
||
playable.
|
||
4. State (the buffer itself) survives the MV3 service-worker idle unload
|
||
cycle without losing video.
|
||
|
||
Scope anchor from ROADMAP.md — Phase 2 (DOM/event privacy), Phase 3 (export
|
||
UX), Phase 4 (smoke verification), and Phase 5 (P1/P2 hardening) are
|
||
explicitly out of scope here.
|
||
|
||
</domain>
|
||
|
||
<decisions>
|
||
## Implementation Decisions
|
||
|
||
### Capture API — AMENDS DEC-003
|
||
|
||
This phase REPLACES the SPEC-locked `chrome.tabCapture` choice with
|
||
`getDisplayMedia()` capture. Done eyes-open: the operator gains broader
|
||
capture coverage at the cost of the SPEC §1 "silent operation" property.
|
||
The doc cascade is enumerated in the **Doc Amendments (precede code)**
|
||
subsection below.
|
||
|
||
- **D-01:** Capture mechanism is `navigator.mediaDevices.getDisplayMedia()`
|
||
invoked **inside the offscreen document**. No more
|
||
`chrome.tabCapture.getMediaStreamId`, no more SW-side gesture juggling.
|
||
- **D-02:** Offscreen document is created with
|
||
`chrome.offscreen.Reason.DISPLAY_MEDIA` (replaces `USER_MEDIA`).
|
||
- **D-03:** One-time source picker on session start; the operator picks
|
||
"screen" or "window" once. If they later click the Chrome "Stop sharing"
|
||
banner or the captured source disappears, the offscreen surfaces an error
|
||
to the SW and the popup re-prompts on next interaction. (Exact error-UX
|
||
copy is deferred to Phase 3 — see Deferred Ideas.)
|
||
- **D-04:** Operator UX is **NOT** silent. Chrome's permanent "Sharing your
|
||
screen" indicator is shown while recording. We accept this as the cost
|
||
of the API choice.
|
||
- **D-05:** `manifest.json` permissions follow the new API: `desktopCapture`
|
||
replaces `tabCapture`; `activeTab` becomes unnecessary for the video
|
||
pipeline but stays for `chrome.tabs.captureVisibleTab` (screenshot path,
|
||
Phase 3 concern — kept).
|
||
|
||
### Offscreen source-of-truth location
|
||
|
||
- **D-06:** Recorder code lives at **`src/offscreen/recorder.ts`** as a real
|
||
TypeScript module with strict type-check, source maps, and IDE support.
|
||
- **D-07:** `offscreen/index.html` is rewritten to load the bundled module
|
||
via crxjs. The runtime path remains `offscreen/index.html` (referenced
|
||
from SW via `chrome.runtime.getURL('offscreen/index.html')`).
|
||
- **D-08:** **DELETE** `offscreen/index.ts` (orphaned dead code) and the
|
||
entire `copy-offscreen` plugin block in `vite.config.ts:11-184`. crxjs
|
||
picks up the new TS entry through the HTML reference.
|
||
|
||
### Ring-buffer mechanism
|
||
|
||
- **D-09:** **Single continuous MediaRecorder** for the whole session.
|
||
`mediaRecorder.start(2000)` so chunks land on cluster boundaries per the
|
||
spec timeslice (DEC-003, SPEC §4.1). No restart strategy at this point.
|
||
- **D-10:** Retain the **first emitted chunk** (the chunk produced by the
|
||
first `dataavailable` event after `start()`) **indefinitely** — it carries
|
||
the EBML header plus the initial cluster. CON-webm-header-retention.
|
||
- **D-11:** Drop later chunks once they are older than 30 s, by chunk
|
||
arrival timestamp. Keep header + every chunk newer than `now - 30000 ms`.
|
||
- **D-12:** Acceptance gate for Phase 1: `ffprobe -v error -f matroska -i
|
||
<last_30sec.webm>` must return exit 0 with no decoder warnings on a
|
||
fresh-export sample. Plan-checker enforces this as a phase success
|
||
criterion.
|
||
- **D-13:** **Fallback if D-12 fails:** revise the plan mid-phase to use
|
||
*restart-segments* (stop + restart the MediaRecorder every 10 s, keep
|
||
the 3 most-recent self-contained segments, concat on save). Documented
|
||
as a known fallback so the planner can pre-stage the alternative
|
||
structure in PLAN.md.
|
||
|
||
### Tab-switch behavior
|
||
|
||
- **D-14:** **Not applicable** under the new capture API. `getDisplayMedia()`
|
||
captures a screen or window, not a tab — there is nothing to re-attach
|
||
on `chrome.tabs.onActivated`. Phase 1 explicitly **removes** any
|
||
tab-switch handling from `src/background/index.ts`.
|
||
- **D-15:** Operator switching tabs no longer interrupts the recording —
|
||
the buffer keeps filling regardless of active tab.
|
||
|
||
### State survival across SW unload
|
||
|
||
- **D-16:** Video buffer **ownership moves to the offscreen document**. The
|
||
offscreen survives SW unloads because it holds the
|
||
`DISPLAY_MEDIA`-reason capture; chunks accumulate there.
|
||
- **D-17:** A long-lived `chrome.runtime.connect` port from offscreen → SW
|
||
serves as the keepalive (this is the only mechanism that actually
|
||
resets the SW idle timer — `chrome.alarms` callbacks do not, contrary
|
||
to DEC-010).
|
||
- **D-18:** **DELETE** the `chrome.alarms` keepalive
|
||
(`src/background/index.ts:171-178`). DEC-010 and CON-service-worker-keepalive
|
||
are amended in the doc cascade below.
|
||
- **D-19:** On export, SW requests the buffer from offscreen over the port
|
||
(or one-shot `chrome.runtime.sendMessage`). SW does **NOT** cache
|
||
chunks. CON-buffer-storage is honored — buffer is plain JS variable in
|
||
offscreen memory, no `chrome.storage.session`, no IndexedDB. The
|
||
existing IndexedDB code path in `vite.config.ts:43-104` is **DELETED**
|
||
along with the inline plugin.
|
||
|
||
### Doc Amendments (precede code)
|
||
|
||
These document edits **MUST** ship before any code-touching task in this
|
||
phase, so downstream phases see a consistent baseline:
|
||
|
||
- **D-A1:** Amend `.planning/intel/decisions.md` DEC-003 to record the
|
||
`getDisplayMedia` replacement, with rationale and the explicit silent-
|
||
operation trade-off. Amend DEC-010 to record port keepalive replacing
|
||
alarms keepalive.
|
||
- **D-A2:** Amend `.planning/intel/constraints.md` to **RETIRE**
|
||
CON-tab-capture-binding and CON-service-worker-keepalive. Add new
|
||
CON-display-capture-binding (one-time picker, "Sharing" indicator).
|
||
- **D-A3:** Amend `.planning/PROJECT.md` Key Decisions table (DEC-003,
|
||
DEC-010) and Constraints section accordingly.
|
||
- **D-A4:** Amend `.planning/REQUIREMENTS.md` REQ-video-ring-buffer to
|
||
remove "active-tab" wording and update API binding.
|
||
- **D-A5:** Amend `.planning/ROADMAP.md` Phase 1 description and Success
|
||
Criterion #2 (drop the "tab re-attach" clause).
|
||
- **D-A6:** Amend `manifest.json`: swap `tabCapture` → `desktopCapture`
|
||
in `permissions`. Keep `activeTab` for the screenshot path.
|
||
|
||
### Claude's Discretion
|
||
|
||
- Exact protocol choice for offscreen↔SW messaging (port for keepalive +
|
||
sendMessage for one-shot vs port-only).
|
||
- Codec strictness: enforce `video/webm; codecs=vp9` via
|
||
`MediaRecorder.isTypeSupported`; fail loud if unsupported (no fallback
|
||
chain — current code's vp9→vp8→h264→default fallback is removed).
|
||
- Internal naming for the new buffer-owning module (offscreen-recorder vs
|
||
display-recorder etc.).
|
||
- Code-style choices around TS strictness within `src/offscreen/`
|
||
(already on `"strict": true` per tsconfig).
|
||
|
||
</decisions>
|
||
|
||
<specifics>
|
||
## Specific Ideas
|
||
|
||
- "Fail loud" on missing `getDisplayMedia` support — log a clear error to
|
||
the SW console with the failing user-agent string. No silent fallback.
|
||
- The ffprobe verification gate is a real CLI invocation in the plan's
|
||
acceptance, not a hand-wave — Phase 1 success requires producing a
|
||
sample WebM and running ffprobe against it.
|
||
- The "stop sharing" recovery path: when the offscreen detects
|
||
`MediaStreamTrack.onended`, it should clear the buffer, send a
|
||
RECORDING_STOPPED message to SW, and stop attempting to record. The
|
||
popup will re-prompt next time the user clicks save (Phase 3
|
||
territory).
|
||
- The `OFFSCREEN_READY` Message type is already declared in
|
||
`src/shared/types.ts:18` but never used. Phase 1 wires up an actual
|
||
handshake so SW doesn't fire `START_RECORDING` before the offscreen
|
||
listener is registered (this fixes audit item P1 #12 in passing).
|
||
|
||
</specifics>
|
||
|
||
<canonical_refs>
|
||
## Canonical References
|
||
|
||
**Downstream agents MUST read these before planning or implementing.**
|
||
|
||
### Project baseline (current state, will be amended by D-A1..D-A6)
|
||
|
||
- `.planning/PROJECT.md` — overall project context. Note that DEC-003,
|
||
DEC-010, and the Constraints section will be amended by this phase.
|
||
- `.planning/REQUIREMENTS.md` §"Video" — REQ-video-ring-buffer wording
|
||
changes in this phase.
|
||
- `.planning/ROADMAP.md` §"Phase 1" — phase definition and success
|
||
criteria (Success Criterion #2 changes).
|
||
- `.planning/intel/SYNTHESIS.md` — entry point for ingested intel.
|
||
- `.planning/intel/decisions.md` §DEC-003, §DEC-009, §DEC-010.
|
||
- `.planning/intel/constraints.md` §CON-video-window, §CON-video-codec,
|
||
§CON-webm-header-retention, §CON-tab-capture-binding (RETIRED),
|
||
§CON-service-worker-keepalive (RETIRED), §CON-buffer-storage.
|
||
|
||
### Authoritative SPEC
|
||
|
||
- `Тз расширение фаза1.md` §2 (stack), §4.1 (video buffer parameters),
|
||
§7 (manifest permissions), §8 row 3 (tab-capture binding — being
|
||
AMENDED), §10 acceptance criteria #2, #3, #7.
|
||
|
||
### Audit
|
||
|
||
- `/home/parf/.claude/plans/dear-claude-there-is-snazzy-fox.md` — full
|
||
P0 analysis. Key spots: P0-1 (offscreen duality + mediaRecorder
|
||
shadow, `vite.config.ts:113`/`vite.config.ts:208`), P0-2 (WebM ring
|
||
buffer + 200 ms timeslice issue), P0-3 (always-on capture), P1 #8
|
||
(alarms keepalive ineffective), P1 #12 (OFFSCREEN_READY handshake
|
||
missing).
|
||
|
||
### Chrome APIs (read before researching)
|
||
|
||
- MDN: `MediaDevices.getDisplayMedia()` — capture semantics, picker UX,
|
||
MediaStreamTrack.onended.
|
||
- Chrome docs: `chrome.offscreen` API, especially `Reason.DISPLAY_MEDIA`
|
||
(Chrome 116+).
|
||
- Chrome docs: Service worker lifecycle in MV3 — confirm `chrome.alarms`
|
||
vs `chrome.runtime.connect` port behavior with respect to idle timer.
|
||
- WebM/Matroska EBML format — cluster boundary semantics; needed only if
|
||
the D-13 fallback (restart-segments) activates.
|
||
|
||
</canonical_refs>
|
||
|
||
<code_context>
|
||
## Existing Code Insights
|
||
|
||
### Reusable assets
|
||
|
||
- `src/background/index.ts:16-73` — `videoBuffer: VideoChunk[]` array +
|
||
`cleanupVideoBuffer()`. Structural pattern (header retention + age
|
||
trim) is **kept**; the implementation is **moved** to
|
||
`src/offscreen/recorder.ts`.
|
||
- `src/shared/types.ts` — `Message`, `VideoChunk`, `VideoBufferResponse`,
|
||
`OFFSCREEN_READY`, `RECORDING_ERROR`. Reuse as-is; wire up
|
||
`OFFSCREEN_READY` and `RECORDING_ERROR` in Phase 1.
|
||
- `src/shared/logger.ts` — `Logger` (SW context) and `ContentLogger`
|
||
(content-script context). Add an `OffscreenLogger` or reuse `Logger`
|
||
with prefix `[Offscreen:...]`.
|
||
|
||
### Established patterns
|
||
|
||
- Strict TS with `noUnusedLocals`/`noUnusedParameters` (`tsconfig.json`).
|
||
New code must comply — no `as any`, no `@ts-ignore` (audit item P1
|
||
#13 informs this).
|
||
- Russian comments inline are acceptable per project provenance
|
||
(see `Тз расширение фаза1.md`, the user-facing UI strings in REQ-popup-ui).
|
||
|
||
### Files to DELETE in this phase
|
||
|
||
- `offscreen/index.ts` — dead code (audit P2 #18).
|
||
- `offscreen/index.html` — REPLACED by a crxjs-managed equivalent
|
||
pointing at the new `src/offscreen/recorder.ts`. (Path stays the same
|
||
in dist; source moves.)
|
||
- `vite.config.ts:11-184` — the entire `copy-offscreen` inline plugin
|
||
block including the embedded JS string + IndexedDB plumbing. Replaces
|
||
with a small note + the standard crx() invocation.
|
||
- `src/background/index.ts:171-178` — `setupKeepalive()` and its
|
||
`chrome.alarms` registration.
|
||
- `src/background/index.ts:445-475` — `loadChunkFromIndexedDB()` and
|
||
`openIndexedDB()` (the SW-side IDB plumbing).
|
||
- `src/background/index.ts:432-442` — the `VIDEO_CHUNK` message handler
|
||
that expected Blob over `chrome.runtime.sendMessage` (the
|
||
fundamentally broken path from audit P0 #2 — never reachable today
|
||
but documented to delete).
|
||
|
||
### Files to CREATE
|
||
|
||
- `src/offscreen/recorder.ts` — new module owning getDisplayMedia +
|
||
MediaRecorder + ring buffer + keepalive port.
|
||
- `src/offscreen/index.html` (or amended `offscreen/index.html`) —
|
||
minimal HTML that loads the new module via crxjs.
|
||
|
||
### Files to MODIFY heavily
|
||
|
||
- `src/background/index.ts` — owns offscreen lifecycle, port handling,
|
||
export-time buffer fetch. Most of the file shrinks.
|
||
- `manifest.json` — permissions swap, `offscreen` already present.
|
||
- `vite.config.ts` — collapse to a clean crxjs invocation, no inline
|
||
plugin.
|
||
|
||
</code_context>
|
||
|
||
<deferred>
|
||
## Deferred Ideas
|
||
|
||
- **Error UX for "user stopped sharing" mid-session.** The popup needs a
|
||
state for this — Phase 3 territory (REQ-popup-ui state machine
|
||
extension).
|
||
- **Audio capture.** `getDisplayMedia()` makes audio capture trivial
|
||
(`audio: true`), but SPEC §9 explicitly excludes audio from Phase 1
|
||
(Phase 2 work — CAP-01). Capture this as an easier-now-than-before
|
||
follow-up.
|
||
- **Per-tab silent capture mode** as an opt-in via `config.json`. Could
|
||
re-introduce tabCapture for installations that prioritize silent
|
||
operation over broad coverage. Future phase if there's demand.
|
||
- **Cluster-aware EBML trim (ts-ebml).** Not needed for Phase 1 if
|
||
continuous + age-trim verifies via ffprobe. Keep on the shelf as a
|
||
third fallback under D-13.
|
||
- **`chrome.storage.session` cold-start recovery.** Buffer pointer
|
||
rehydration after offscreen crash. Phase 5 (Harden + clean up)
|
||
territory.
|
||
|
||
</deferred>
|
||
|
||
---
|
||
|
||
## Amendment (Phase 01-stabilize-video-pipeline, 2026-05-16) — D-17-port-lifecycle
|
||
|
||
- **AMENDED-BY:** debug session `empty-archive-port-race` (Option C, resolved 2026-05-16)
|
||
- **Replaces nothing.** D-17 above stands as the original port-keepalive
|
||
contract. This amendment narrows the port LIFECYCLE shape that was
|
||
Claude's-discretion at Phase 1 plan time.
|
||
- **Background.** UAT Test 3 surfaced two coupled defects: 3× Uncaught
|
||
"Attempting to use a disconnected port object" in the offscreen console
|
||
starting at the 290 s pre-emptive reconnect mark, AND a silent
|
||
empty-video archive shipping to the operator. Bisect proved the H1
|
||
port race was introduced by Plan 01-04 (commit `b064a21`) and the H2
|
||
silent-skip in `createArchive` was an upstream defect from commit
|
||
`555eb05` that the port race amplified from latent to fatal. Full
|
||
lineage and strategy rationale: `.planning/debug/resolved/empty-archive-port-race.md`.
|
||
- **Architectural commitments retired:**
|
||
- The legacy 290 000 ms pre-emptive `setTimeout` reconnect (Pitfall 4)
|
||
is RETIRED. Its race window between the synchronous `.disconnect()`
|
||
and the `onDisconnect` handler firing was the proximate cause of
|
||
H1 — see the bisect notes.
|
||
- **Architectural commitments added:**
|
||
- **Port health probe (PING ↔ PONG).** The offscreen `PORT_PING_MS`
|
||
interval doubles as a liveness probe; each PING expects a PONG echo
|
||
from the SW. The offscreen tracks `missedPongs` and reconnects
|
||
cleanly when the count exceeds `MAX_MISSED_PONGS = 3` (~75 s of
|
||
unresponsive SW — well past Chrome's ~30 s idle threshold, so a
|
||
real disconnect would already have surfaced its own
|
||
`onDisconnect`). The SW echoes PONG on every PING via the
|
||
onConnect-level message sink.
|
||
- **Request-id'd REQUEST_BUFFER / BUFFER.** Every `REQUEST_BUFFER`
|
||
carries a SW-generated `requestId` (crypto.randomUUID with
|
||
Math.random fallback). The offscreen echoes the same `requestId`
|
||
on the BUFFER response. The SW routes BUFFER → pending request via
|
||
a module-level `Map<requestId, PendingBufferRequest>` — port-
|
||
agnostic, so port replacement does not lose the response.
|
||
- **Retry on port replacement.** Every `onConnect` (post-bootstrap)
|
||
scans `pendingBufferRequests` and re-issues REQUEST_BUFFER on the
|
||
fresh port with the SAME requestId. The offscreen posts BUFFER on
|
||
the CURRENT `keepalivePort`, the sink matches by id, and the
|
||
request resolves. This retires the H2 silent-drop class
|
||
architecturally.
|
||
- **Outer hard-timeout bumped 2 s → 10 s.** The legacy per-port
|
||
`BUFFER_FETCH_TIMEOUT_MS = 2000` was too tight to cover a retry
|
||
across a reconnect. The new outer budget covers EVERY retry across
|
||
port replacements; the inner per-port round-trip is still
|
||
~100-200 ms.
|
||
- **Operator-visible failure surface.** `createArchive` throws
|
||
`EmptyVideoBufferError` when the video buffer is empty. `saveArchive`
|
||
catches and emits `{type:'RECORDING_ERROR', error:'empty-video-buffer'}`
|
||
via `chrome.runtime.sendMessage` for the popup, AND returns
|
||
`{success:false, error}` in the direct-response path. Replaces the
|
||
upstream silent-skip branch in `createArchive` that shipped a
|
||
video-less archive in silence.
|
||
- **Pinning contracts added:**
|
||
- `tests/offscreen/port-health-probe.test.ts` — pins the PING/PONG +
|
||
request-id'd encode contract on the offscreen side (4 tests).
|
||
- `tests/background/request-id-protocol.test.ts` — pins the SW-side
|
||
request-id routing + retry + error-surface contract (5 tests).
|
||
- `tests/background/port-lifecycle-continuous.test.ts` — continuous
|
||
600 s end-to-end simulation: 12 ping/pong cycles + 2 forced
|
||
reconnects + 3 SAVE_ARCHIVE round-trips, asserts no Uncaught and
|
||
every BUFFER carries segments.
|
||
- `tests/offscreen/port-reconnect-race.test.ts` (refactored): H1.b
|
||
no longer pins the retired 290 s setTimeout path — it now pins
|
||
the externally-disconnected-port → ping try/catch → reconnect path
|
||
that the H1 fix delivers.
|
||
|
||
---
|
||
|
||
## Amendment (Phase 01-stabilize-video-pipeline, 2026-05-16 second batch) — Plans 01-08 / 01-09 / 01-10 charter
|
||
|
||
**Context.** On 2026-05-16 Phase 1 was REOPENED after UAT Test 3 re-attempt
|
||
revealed two compounding gaps in the 2026-05-15 closure:
|
||
|
||
1. D-13's concat-of-self-contained-WebM-segments architecture produces a
|
||
multi-EBML-header file that mpv, Chrome's HTMLMediaElement, and
|
||
ffprobe's `format=duration` all play as ~9.94 s (the first segment's
|
||
Info.Duration only) instead of ~30 s. The "operator-confirmed clean
|
||
Chrome playback" check from 2026-05-15 verified playback ran without
|
||
freezing but did not measure total duration. SPEC §10 #7
|
||
(`last_30sec.webm plays back in a browser`) is not actually satisfied.
|
||
See `.planning/debug/d13-multi-ebml-concat-unplayable.md` for the
|
||
byte-level EBML probe + library-survey + decision rationale.
|
||
|
||
2. The operator UX is unsatisfactory at the picker stage (tab-share
|
||
footgun: "Share this tab instead" affordance) and at the activation
|
||
stage (3 clicks per session: toolbar → popup open → Start button →
|
||
picker). These were flagged in UAT Test 3's advisory section and
|
||
deferred to Phase 5 at closure time; the reopen reclassifies them
|
||
as Phase 1 deliverables because the same fix-cycle window is
|
||
available.
|
||
|
||
The orchestrator's "add-more-plans" routing on 2026-05-16 adds three
|
||
plans (01-08, 01-09, 01-10) to close these gaps without re-litigating
|
||
the seven plans (01-01..01-07) that already landed.
|
||
|
||
**D-14-remux: WebM remux via ts-ebml + webm-muxer (supersedes D-13's file-concat).**
|
||
|
||
- **AMENDED-BY:** debug session `d13-multi-ebml-concat-unplayable`
|
||
(root-cause confirmed 2026-05-16). Remediation lands via Plan 01-08.
|
||
- **Replaces partial.** D-13's recorder-side restart-segments lifecycle
|
||
(the part that fixed the orphan-P-frame freeze observed in debug
|
||
session `webm-playback-freeze`) is PRESERVED. Only D-13's file-concat
|
||
merge on the SW save path is retired. Original D-13 text above
|
||
remains intact for provenance.
|
||
- **Architectural commitments retired:** D-13 file-concat byte-stream
|
||
merge in `src/background/index.ts:mergeVideoSegments` is RETIRED.
|
||
Concat of self-contained WebM segments does NOT produce a single
|
||
playable 30 s WebM in any common consumer-grade player.
|
||
- **Architectural commitments added:**
|
||
- `remuxSegments(segments: VideoSegment[]): Promise<Blob>` in
|
||
`src/background/webm-remux.ts` replaces `mergeVideoSegments`.
|
||
Parses each segment via `ts-ebml`'s Decoder, walks the EBML tree,
|
||
extracts each Cluster's SimpleBlock children (VP9 frame bytes +
|
||
keyframe flag + cluster Timecode + block offset), and re-mixes
|
||
into a single output WebM via `webm-muxer`'s `Muxer<ArrayBufferTarget>.addVideoChunkRaw(data, type, timestampUs)`.
|
||
Each frame's timestamp is adjusted to be monotonic against a
|
||
single global timeline.
|
||
- **Library choice (LOCKED):** `ts-ebml` ^3.0.2 (parse) +
|
||
`webm-muxer` ^5.1.4 (write). Both MIT, both actively maintained
|
||
(last releases 2025-09-28 and 2025-07-02 respectively), both
|
||
SW-compatible (no DOM globals on the hot path). Combined gzipped
|
||
weight ~100 KB. Verified by `tests/background/webm-remux-deps.test.ts`.
|
||
- **`createArchive` becomes async on the merge path.**
|
||
`videoBlob = await remuxSegments(...)` replaces synchronous
|
||
`mergeVideoSegments(...)`. The existing `EmptyVideoBufferError`
|
||
throw (Option C, D-17-port-lifecycle amendment) is preserved AND extended to
|
||
fire on a zero-byte remux output.
|
||
- **D-13 recorder-side lifecycle UNCHANGED.** The offscreen-side
|
||
`src/offscreen/recorder.ts` keeps the restart-segments rotation
|
||
(`SEGMENT_DURATION_MS = 10_000` ms, `MAX_SEGMENTS = 3`), which
|
||
remains the canonical fix for the orphan-P-frame freeze from
|
||
debug session `webm-playback-freeze`. The remux happens only on
|
||
export, only in the SW.
|
||
- **Pinning contracts added (Plan 01-08):**
|
||
- `tests/background/webm-remux-deps.test.ts` (2 tests — library
|
||
presence + SW-compat).
|
||
- `tests/background/webm-remux.test.ts` (5 tests — single-EBML
|
||
invariant, monotonic timestamps, size sanity, ffprobe duration
|
||
>= 25 s, frame-count tolerance).
|
||
- `tests/offscreen/webm-playback.test.ts` lines 231-316 (the 2 RED
|
||
tests landed by the d13 debug session) flip GREEN against the
|
||
regenerated `tests/fixtures/last_30sec.webm`.
|
||
- **D-13 status:** PARTIALLY RETIRED. Recorder lifecycle preserved;
|
||
file-concat merge retired. CONTEXT.md D-13 text above stands for
|
||
historical provenance; downstream readers reaching D-13 must ALSO
|
||
read this D-14-remux amendment.
|
||
|
||
**D-15-display-surface: Whole-desktop constraint + post-grant validation (replaces
|
||
operator-discretion picker).**
|
||
|
||
- **AMENDED-BY:** UAT Test 3 advisory finding (2026-05-16): "Share
|
||
this tab instead" Chrome affordance is a one-click footgun that
|
||
redirects the recording target mid-session. Reclassified from
|
||
Phase 5 advisory to Phase 1 deliverable via Plan 01-09.
|
||
- **Replaces nothing structurally.** D-01's `getDisplayMedia` choice
|
||
stands; this amendment narrows the constraints object passed to it.
|
||
- **Architectural commitments added:**
|
||
- `getDisplayMedia` is invoked with constraints
|
||
`{video: { displaySurface: 'monitor', cursor: 'always' }, audio: false}`.
|
||
The `displaySurface: 'monitor'` constraint hints Chrome's picker
|
||
to default to entire-screen selection; the `cursor: 'always'`
|
||
constraint includes the operator's screen cursor in captured
|
||
frames (this lifts the Phase 5 cursor-visibility refinement
|
||
opportunistically — STATE.md Decisions entry [Phase 01-07-deferred-to-5]).
|
||
- Post-grant validation reads `track.getSettings().displaySurface`.
|
||
If the operator overrode the hint and picked a tab or window,
|
||
the recorder tears down the stream, nulls `mediaStream`, and
|
||
throws `Error('wrong-display-surface: got "<observed>"')`. The
|
||
throw routes through `classifyCaptureError` (extended with a
|
||
`'wrong-display-surface'` branch added to the `CaptureErrorCode`
|
||
union) into the existing `RECORDING_ERROR` channel.
|
||
- The `CaptureErrorCode` union grows by one member:
|
||
`'wrong-display-surface'` (joins the existing seven).
|
||
- **Pinning contract added (Plan 01-09):**
|
||
- `tests/offscreen/display-surface-constraint.test.ts` (4 tests:
|
||
constraints applied; wrong-displaySurface emits RECORDING_ERROR;
|
||
monitor-displaySurface does NOT emit; classifyCaptureError
|
||
branch).
|
||
|
||
**D-16-toolbar: Toolbar-onClicked + popup-SAVE-only + badge state machine +
|
||
onStartup/recovery notifications (replaces popup-Start-button activation).**
|
||
|
||
- **AMENDED-BY:** UAT Test 3 ergonomics observation; operator-experience
|
||
goal "per-session click count from 3 to 2" surfaced by user on
|
||
2026-05-16. Reclassified from soft-deferred UX-polish to Phase 1
|
||
deliverable via Plan 01-09.
|
||
- **Replaces nothing structurally.** The existing popup → SW
|
||
`REQUEST_PERMISSIONS` → `startVideoCapture` flow stands; this
|
||
amendment changes WHICH UI surface triggers REQUEST_PERMISSIONS.
|
||
- **Architectural commitments added:**
|
||
- `chrome.action.onClicked` listener registered at SW module load.
|
||
When `isRecording === false`, the handler invokes
|
||
`startVideoCapture()` directly (the toolbar click counts as the
|
||
user gesture for `getDisplayMedia`).
|
||
- Dynamic `chrome.action.setPopup` swap: empty string (`''`) in
|
||
OFF mode (so toolbar click triggers `onClicked`); pointing to
|
||
`src/popup/index.html` in REC mode (so toolbar click opens the
|
||
popup for SAVE). The SAVE-only popup retires the auto-prompt
|
||
behavior in `src/popup/index.ts:checkPermissions`.
|
||
- Badge state machine with three states. REC: green background
|
||
(`#00C853`), text `'REC'`, tooltip "Recording — last 30 s
|
||
buffered. Click to save." OFF: red background (`#D32F2F`),
|
||
empty text, tooltip "Not recording. Click to start." ERROR:
|
||
yellow background (`#F9A825`), text `'ERR'`, tooltip
|
||
"Recording error. Click to try again."
|
||
- `chrome.runtime.onStartup` fires `chrome.notifications.create`
|
||
once per browser startup with a `'mokosh-startup-'`-prefixed id,
|
||
inviting the operator to click to start.
|
||
- `chrome.notifications.onClicked` validates the id prefix
|
||
(`'mokosh-'`), drains the notification via `chrome.notifications.clear`,
|
||
and triggers `startVideoCapture()` (notification click is also
|
||
a user gesture under Chrome's documented activation model).
|
||
- On `RECORDING_ERROR` receipt, the SW transitions badge to ERROR
|
||
and emits a `'mokosh-recovery-'`-prefixed recovery notification
|
||
inviting a fresh start.
|
||
- `manifest.json` adds `notifications` to permissions. `default_popup`
|
||
retained for the SAVE flow.
|
||
- **Popup role change.** `src/popup/index.ts` no longer auto-requests
|
||
permissions on init. `checkPermissions` / `requestPermissions`
|
||
functions are removed from the init path. Empty-state copy updated
|
||
to direct operator to the toolbar icon.
|
||
- **Smoke harness update.** `smoke.sh`'s auto-select target string
|
||
changes from the tab title to an entire-screen string (e.g.
|
||
`"Entire screen"` or `"Screen 1"` depending on Chrome locale). If
|
||
the auto-select fails under a non-English Chrome, a one-time
|
||
manual pick is documented as the fallback.
|
||
- **Pinning contracts added (Plan 01-09):**
|
||
- `tests/background/toolbar-action.test.ts` (4 tests: onClicked
|
||
routing; setPopup dance).
|
||
- `tests/background/badge-state-machine.test.ts` (4 tests: three
|
||
badge states + RECORDING_ERROR transition).
|
||
- `tests/background/onstartup-notification.test.ts` (4 tests:
|
||
onStartup → create notification; onClicked → start recording;
|
||
RECORDING_ERROR → recovery notification).
|
||
|
||
**D-17 (NEW — distinct from the port-lifecycle D-17 amendment above):
|
||
Onboarding welcome tab on first install.**
|
||
|
||
> NB: this is a SEPARATE D-17 marker, scoped to Plan 01-10. The earlier
|
||
> D-17 amendment block (port lifecycle) is also labeled D-17 in its
|
||
> historical context (it amends the original D-17 from the 2026-05-15
|
||
> decisions block above). To disambiguate downstream readers, this
|
||
> Plan 01-10 marker is referenced as D-17-onboarding in cross-citations;
|
||
> the port-lifecycle marker is referenced as D-17-port-lifecycle.
|
||
|
||
- **Trigger:** Plan 01-09's UX is operator-facing at runtime; Plan 01-10
|
||
adds the install-time activation path so the operator's very first
|
||
interaction with the extension is also one-click.
|
||
- **Architectural commitments added:**
|
||
- `chrome.runtime.onInstalled` handler extended: on
|
||
`details.reason === 'install'` AND `chrome.storage.local`
|
||
`'onboarding-completed'` flag absent, open
|
||
`src/welcome/welcome.html` via
|
||
`chrome.tabs.create({url: chrome.runtime.getURL(...)})`. After the
|
||
tab opens, set the `'onboarding-completed'` flag so future installs
|
||
or reloads do NOT re-open the welcome.
|
||
- New welcome page bundle: `src/welcome/welcome.{html,ts,css}`.
|
||
Vanilla DOM per project style; single `'Начать запись'` button
|
||
that sends `REQUEST_PERMISSIONS` (the install-time click counts
|
||
as user gesture for `getDisplayMedia`).
|
||
- `manifest.json` adds `web_accessible_resources` array with entry
|
||
for `src/welcome/welcome.html` so `chrome.runtime.getURL` resolves.
|
||
`'storage'` permission already present (no change needed).
|
||
- `vite.config.ts` `rollupOptions.input` gains a `welcome:
|
||
'src/welcome/welcome.html'` entry so crxjs bundles the page.
|
||
- **Pinning contract added (Plan 01-10):**
|
||
- `tests/background/onboarding.test.ts` (3 tests: first install
|
||
creates welcome tab; subsequent install does NOT; already-completed
|
||
flag suppresses).
|
||
|
||
---
|
||
|
||
*Phase: 01-stabilize-video-pipeline*
|
||
*Context gathered: 2026-05-15*
|
||
*Amended: 2026-05-16 (debug session empty-archive-port-race, Option C — D-17-port-lifecycle narrowing)*
|
||
*Amended: 2026-05-16 (Plans 01-08 / 01-09 / 01-10 charter — D-14-remux WebM remux supersedes D-13 file-concat; D-15-display-surface whole-desktop + cursor; D-16-toolbar toolbar + badge + notifications; D-17-onboarding welcome tab)*
|