Commit Graph

263 Commits

Author SHA1 Message Date
349ae88a8e fix(01-review): WR-04+WR-05 smoke.sh require python3 + identity-based zip detection 2026-05-16 10:19:46 +02:00
650c546a6e fix(01-review): WR-01+WR-02 stable capture error codes + pure assertCodecSupported 2026-05-16 09:49:01 +02:00
7bc2ef8c38 wip: phase-01 paused at review-fix 5/18; 13 findings + 8 sweep targets remain .planning/phases/01-stabilize-video-pipeline/.continue-here.md .planning/HANDOFF.json 2026-05-16 09:30:16 +02:00
2e3f5248ce fix(01-review): CR-01+CR-02+CR-03+WR-03+WR-09 critical port + handshake race fixes
What was wrong:
- CR-01 (recorder.ts): encodeAndSendBuffer captured no port identity before
  awaiting Promise.all(blobToBase64). If the port disconnected mid-encode
  and onDisconnect synchronously reconnected (re-assigning keepalivePort
  to a fresh instance), the post-await null-check evaluated false and
  the BUFFER was posted on the NEW port — but the SW's per-request
  onMessage listener was still bound to the OLD port (captured at
  getVideoBufferFromOffscreen line 110). Result: SW timed out after
  2 s, SAVE_ARCHIVE produced an empty-segments zip, data-loss path
  masquerading as a benign timeout.
- CR-02 (background/index.ts): SW's onConnect handler attached
  ONLY onDisconnect — no permanent onMessage sink. PING traffic
  had no listener when getVideoBufferFromOffscreen wasn't running
  (the normal idle state of the port), and field reports note Chrome's
  SW idle-timer reset behaves inconsistently when no listener is
  attached. Risk: PINGs silently dropped, SW evicted ~30 s into
  recording, port torn down, next SAVE_ARCHIVE fails entirely.
- CR-03 (background/index.ts): offscreenReady is a one-shot Promise
  resolved on the FIRST OFFSCREEN_READY message. If the SW is evicted
  while the offscreen document persists, the next SW lifetime creates
  a fresh Promise and waits on it forever — the offscreen never
  re-emits OFFSCREEN_READY. startVideoCapture() hangs at
  `await offscreenReady` until Chrome restarts.
- WR-03 (recorder.ts): `baseTimestamp + idx` (Date.now() + idx) used
  millisecond resolution + array offset. Two REQUEST_BUFFER calls
  within the same millisecond would collide, breaking the sort-by-
  timestamp contract in SW-side mergeVideoSegments.
- WR-09 (recorder.ts): encodeAndSendBuffer always appended the
  unfinalized in-flight segment to the BUFFER. That segment lacks
  the Matroska SegmentSize and Cues that MediaRecorder.stop()
  writes — re-introducing the "File ended prematurely" symptom
  documented in debug session webm-playback-freeze.

What changed:
- recorder.ts encodeAndSendBuffer:
  - Capture `portAtRequest = keepalivePort` BEFORE the encode.
  - After the await, refuse to post if `keepalivePort !== portAtRequest`
    (port was replaced by reconnect). SW already times out cleanly
    after BUFFER_FETCH_TIMEOUT_MS = 2 s; the next SAVE_ARCHIVE
    re-issues REQUEST_BUFFER on the fresh port. Stale data
    NEVER reaches a stranger port.
  - Include the in-flight segment ONLY when finalized.length === 0
    (preserve the SAVE-within-first-10-s UX trade-off documented at
    the original comment) — otherwise drop the unfinalized tail.
  - Replace `baseTimestamp + idx` with module-level monotonic
    `++segmentSeq` counter (zero wall-clock dependency).
  - Switch from Promise.all/map+filter to a sequential for-loop
    because each iteration now mutates the shared `segmentSeq`;
    Promise.all timing would interleave assignments. Throughput
    impact negligible (3 segments × ~50 ms base64 each ≈ 150 ms
    vs ~50 ms parallel — still well under the 2 s SW budget).

- background/index.ts onConnect:
  - Install a permanent `port.onMessage.addListener` that
    explicitly drains PING and silently drops unknown traffic.
    Per-request BUFFER listener still wins because it's attached
    LATER in the listener chain when getVideoBufferFromOffscreen
    fires; this sink only catches the idle PING stream and
    guarantees the SW idle-timer reset is consumed by a real
    handler.

- background/index.ts initialize():
  - When `chrome.offscreen.hasDocument()` returns true on SW init,
    immediately resolve `offscreenReady` AND null
    `offscreenReadyResolve`. The offscreen MUST have completed its
    bootstrap before it was observable via hasDocument(); waiting
    for an OFFSCREEN_READY that will never come is a deadlock.

Why these fixes vs alternatives:
- CR-01: alternatives considered: (a) cancel encoding when port
  disconnects (requires AbortController plumbing into blobToBase64);
  (b) re-route the BUFFER through the new port via a per-port
  request-id correlation. Both add machinery for a case the SW
  already handles correctly (2 s timeout → retry). The capture-
  identity check is the minimum-mechanism fix and matches REVIEW.md
  CR-01 fix guidance exactly.
- CR-02: alternative considered: documenting "rely on kernel-level
  port-message side effect for idle-timer reset" — REJECTED, this
  is what the existing comment did and the field evidence shows
  it's unreliable. Explicit listener is the safe default.
- CR-03: alternative considered: (a) have offscreen re-emit
  OFFSCREEN_READY on every inbound SW message — adds noise to
  the message bus and races with the original-bootstrap-emit.
  Option (b) (resolve-on-hasDocument-true) is simpler, narrower,
  and was explicitly recommended by REVIEW.md.
- WR-03: alternative considered: keeping Date.now() and adding a
  microsecond-resolution offset via performance.now() — fragile
  across SW respawns where performance.now() resets. Module-level
  monotonic counter has zero wall-clock dependency.
- WR-09: alternative considered: forcing a synchronous rotation
  at REQUEST_BUFFER time. Rejected — adds ~50–200 ms latency to
  every save AND races with scheduleRotation()'s timer. The
  "exclude unless empty" trade-off matches REVIEW.md option (a)
  exactly and preserves the documented first-10-s UX path.

Validation evidence:
- npx tsc --noEmit: exit 0 (no type errors).
- npx vitest run --reporter=dot: 30/30 tests pass in 2.67 s
  (8 test files, including port.test.ts which pins the reconnect
  invariant and port-serialization.test.ts which pins the wire format).
- grep "as any\|@ts-ignore" src/offscreen/ src/background/index.ts
  src/shared/: no matches (type-safety gate stays clean).
2026-05-16 09:21:34 +02:00
bf00bf65aa docs(01): add code review report (3 critical, 9 warning, 6 info) .planning/phases/01-stabilize-video-pipeline/01-REVIEW.md 2026-05-15 22:35:00 +02:00
1d06d9d70b docs(01-07): write Plan 07 SUMMARY and close phase 1 artifacts
Plan 07 SUMMARY — manual smoke + ffprobe D-12 gate + A3 empirical-playback
gate, three-attempt closure narrative with two pre-staged fallback
activations (base64 wire-format from CONTEXT.md Doc Cascade; D-13
restart-segments from CONTEXT.md), and the process-retro candidate flag.

Structure:

- Frontmatter: dependency graph (requires Plans 01..06; provides D-12/A3
  gates + fixture + REQ closure + D-09..D-11/CON-webm-header-retention
  retirement); patterns established (pre-staged contingency activation;
  three-attempt acceptance); requirements-completed [REQ-video-ring-buffer]
- Accomplishments: D-12 + A3 gates green; 30/30 tests; tsc clean; REQ
  marked Complete; cursor-visibility deferred to Phase 5
- Task Commits: this closure cycle (cd61cbc fixture + 7df72aa REQ/STATE/
  ROADMAP flip + this SUMMARY), plus the prior D-12 fix cycle (5 commits
  c0d9166..bf07619) and A3 fix cycle (6 commits 5530292..872f25d)
- Deviations from ORIGINAL Plan 07: two architectural escalations, both
  resolved by pre-staged fallbacks without re-planning; the closure
  ceremony itself split into 3 atomic commits (spec'd as 1) for
  reviewability
- Issues Encountered: three-attempt journey documented (attempt 1 75-byte
  text → D-12 fix; attempt 2 ffprobe-valid freezes → A3 fix; attempt 3
  clean)
- Process Observation: candidate retro for /gsd-plan-phase to auto-inject
  empirical-acceptance gates BEFORE merging a phase when RESEARCH.md flags
  HIGH-risk assumptions
- Next Phase Readiness: Phase 2 constraints (no competing keepalives;
  T-1-04 sender-check respected) + workflow next steps (code-review +
  verifier per user settings)
- Self-Check: PASSED — all closure artifacts present and verified

Phase 1 is complete; next workflow steps are /gsd-code-review +
/gsd-verify-work, then /gsd-plan-phase 2.
2026-05-15 22:02:11 +02:00
7df72aaa60 feat(01-07): close Phase 1 — REQ-video-ring-buffer complete, SPEC §10 #7 satisfied
Phase 1 closure 2026-05-15 — both acceptance gates green:

- D-12 ffprobe structural gate: `ffprobe -v error -f matroska -i
  tests/fixtures/last_30sec.webm` exit 0 (cd61cbc)
- A3 empirical-playback gate: operator confirmed clean end-to-end Chrome
  playback (no ~1 s freeze); ffmpeg `-v warning -i fixture -f null -`
  exit 0 with zero decoder errors (only expected muxer DTS-monotonicity
  warnings at segment join boundaries — documented D-13 trade-off for
  multi-EBML-header WebM concat; Chrome's MSE pipeline handles this
  natively, satisfying SPEC §10 #7)

Changes:

- .planning/REQUIREMENTS.md
  * REQ-video-ring-buffer checkbox [ ] -> [x]; description AMENDED to
    document the D-13 restart-segments lifecycle replacing D-09..D-11;
    SPEC §10 #2, #3, #7 noted as green 2026-05-15
  * Traceability table row: REQ-video-ring-buffer | Phase 1 | Complete
    (was Pending)

- .planning/ROADMAP.md
  * Phase 1 list-item flipped [ ] -> [x] with closure date + summary
  * Phase 1 Success Criteria 1, 2, 3 individually checked off; criterion 2
    re-worded to reflect D-13 segment-cycling (replacing the original
    single-continuous-recorder wording from D-09..D-11)
  * Plan list: 01-07-PLAN.md flipped [ ] -> [x] with closure note
  * Progress table row: 7/7 Complete 2026-05-15 (was 6/7 In Progress)
  * Phase 5 P1/P2 list: appended `getDisplayMedia` cursor visibility
    constraint (`video: { cursor: 'always' }`) — surfaced as user
    observation during Phase 1 smoke 2026-05-15

- .planning/STATE.md
  * Frontmatter: status -> phase_complete, completed_phases 0 -> 1,
    completed_plans 6 -> 7, percent 86 -> 100; stopped_at + last_activity
    rewritten for closure narrative
  * Current Position: Phase 1 COMPLETE, next Phase 2 of 5, Plan 7/7
    complete, progress bar [██████████] 100% Phase 1
  * Performance Metrics: Phase 1 row populated (7 plans, ~50 min); added
    Plan 07 row with closure narrative incl. the two debug sessions
  * Decisions log: appended [Phase 01-07-closure] decision and
    [Phase 01-07-deferred-to-5] note for the cursor-visibility refinement
  * Session Continuity: rewritten for closure; resume file points to the
    Plan 07 SUMMARY (commit 3)
  * Added "Phase 1 Closure Notes" block with ffprobe + ffmpeg gates,
    fixture metadata, test counts, criteria green-status, and a process
    retro candidate (auto-injection of empirical-acceptance gates when
    RESEARCH.md flags HIGH-risk assumptions)

Refs: .planning/debug/resolved/d12-blob-port-transfer-fails.md,
.planning/debug/resolved/webm-playback-freeze.md
2026-05-15 21:58:38 +02:00
cd61cbccb6 test(01-07): commit regenerated last_30sec.webm fixture against D-13 recorder
Replaces the stale fixture committed in 87909d9 (test(fix-a3): commit
debug-session test artifacts + stale fixture) — that one was captured
under the D-09..D-11 single-continuous-recorder lifecycle and froze
~1 s into playback per the resolved A3 cluster-alignment session.

This fresh fixture (1.6 MB, VP9 1142×1038) was produced by ./smoke.sh
against the D-13 restart-segments recorder (commits 5530292..872f25d):
three self-contained 10 s WebM segments concatenated by the SW into a
multi-EBML-header file the operator confirmed plays cleanly in Chrome.

Acceptance evidence:
- ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm → exit 0
- ffmpeg -v warning -i tests/fixtures/last_30sec.webm -f null - → exit 0
  with zero decoder errors; only the expected muxer DTS-monotonicity
  warnings at segment join boundaries (non-blocking — multi-EBML-header
  concat is the documented D-13 trade-off)
- Operator confirmed end-to-end Chrome playback clean (no ~1 s freeze)
- All 30/30 unit tests green incl. empirical webm-playback.test.ts gate

This closes the D-12 + A3 acceptance gates referenced in:
- .planning/debug/resolved/d12-blob-port-transfer-fails.md
- .planning/debug/resolved/webm-playback-freeze.md
2026-05-15 21:42:13 +02:00
872f25d649 docs(fix-a3): resolve webm-playback-freeze debug session, update STATE
Closes the second debug session in Phase 1's life (after d12). Both
sessions resolved fast — ~30 min for d12, ~15 min for the RED-test
landing in this one — because the planner had explicitly pre-staged
contingencies (D-12 ffprobe gate + D-13 restart-segments skeleton)
for the assumptions RESEARCH.md flagged HIGH-risk. Neither was a
planning oversight; both were the documented HIGH-risk assumption
activating as expected.

Changes:
- Moved .planning/debug/webm-playback-freeze.md →
  .planning/debug/resolved/webm-playback-freeze.md (status:
  root-cause-confirmed → resolved).
- Added the Resolution section: root-cause one-liner, applied-fix
  description, the 5 files-changed list, the 6 fix-a3 commit hashes,
  the in-tree verification matrix, and the explicit operator
  next-step (re-run ./smoke.sh, verify Chrome playback +
  ffmpeg-clean stderr + the 2 webm-playback.test.ts assertions
  flipping GREEN, then Phase 1 closes).
- Updated STATE.md frontmatter `stopped_at`, the Decisions log
  with a [Phase 01-07-debug-a3] entry summarising D-13 activation
  + the type renames + the retired old-API surface, and the
  Session Continuity block (timestamp, stopped_at narrative,
  resume-file pointer).

Phase 1 close is still pending operator regen of
tests/fixtures/last_30sec.webm. REQ-video-ring-buffer must not
be marked complete by this commit — Plan 07's §10 #7 acceptance
criterion owns that and only the in-Chrome playback + ffmpeg-clean
stderr (against a freshly regenerated fixture) can close it.
2026-05-15 21:18:36 +02:00
87909d976c test(fix-a3): commit debug-session test artifacts + stale fixture
Captures the RED contracts that the webm-playback-freeze debug
session landed (before this fix-a3 cycle started) plus the original
Plan 07 smoke fixture they run against. None of these files were
modified by this fix cycle — they are landed as-is from the debug
session to make the test history bisectable.

Files staged:
- tests/offscreen/segment-keyframes.test.ts
  Three describe blocks (~340 LOC):
    * documentation — pure-simulation tests that pin the D-09..D-11
      failure mode as executable evidence (regression guard against
      re-introducing single-continuous-recorder semantics)
    * GREEN-pinning — pure-simulation tests that pin the D-13
      segment-keyframe invariant
    * production-driven — imports src/offscreen/recorder.ts and
      asserts (i) `getSegments` exported as a function, (ii) it
      returns at most 3 Blobs. THIS BLOCK IS NOW GREEN after the
      D-13 activation in the prior commits — was the genuine TDD
      anchor for fix-a3.
- tests/offscreen/webm-playback.test.ts
  Two empirical-ffmpeg assertions on tests/fixtures/last_30sec.webm:
    * zero "Error submitting packet to decoder" lines from the
      VP9 decoder
    * no "File ended prematurely" container-finalization error
  Both STAY RED in this commit because the committed fixture is
  still the stale one from Plan 07's pre-fix smoke. They flip
  GREEN after the operator runs ./smoke.sh to regenerate the
  fixture against the D-13 recorder — see the closing message
  and the NEXT-STEP block of the resolved debug session.
- tests/fixtures/last_30sec.webm
  The 2.1 MB Plan 07 smoke artifact. Committed deliberately so
  the empirical RED test has something to run against. Will be
  overwritten by the next ./smoke.sh run (single-file rotation —
  the path is fixed by the smoke script + zip extraction step
  in the debug-session reproduction).

Verification:
- npx vitest run --reporter=dot → Tests 2 failed | 28 passed (30)
- The 2 fails are EXACTLY the two empirical-ffmpeg assertions in
  webm-playback.test.ts; the structural production-driven block
  in segment-keyframes.test.ts is fully GREEN.
- npx tsc --noEmit clean.
- npm run build succeeds.

Operator action required before Phase 1 close (Plan 07 still owns
REQ-video-ring-buffer): re-run ./smoke.sh per the documented
6-step reproduction in the debug session, then re-run
`npx vitest run tests/offscreen/webm-playback.test.ts` and
expect both assertions to flip GREEN. Plan 07 success criterion
§10 #7 (playback) lands at that point.
2026-05-15 21:16:02 +02:00
f81438d6c8 feat(fix-a3): rename TransferredVideoChunk → TransferredVideoSegment
Pure rename pass — zero behavioural change at any call site. The
prior "chunk" naming is a vestige of D-09..D-11's chunk-level
buffer; under D-13 the unit of transfer is a self-contained ~10 s
WebM segment, so the type name now matches the data shape.

Renames propagated atomically:
- src/shared/types.ts
  * TransferredVideoChunk → TransferredVideoSegment
    (also: the `isFirst?: boolean` field is dropped — D-13 segments
    are all implicitly their own header, so the flag is meaningless
    and only existed for the retired ring-buffer's pin semantics)
  * VideoChunk → VideoSegment (drops `isFirst?` for the same reason)
  * PortMessage.chunks? → PortMessage.segments?
  * VideoBufferResponse.chunks → VideoBufferResponse.segments
- src/offscreen/recorder.ts
  * import type rename
  * encodeAndSendBuffer's per-element accumulator + filter type
  * the port.postMessage payload field
- src/background/index.ts
  * import type rename
  * getVideoBufferFromOffscreen reads `(msg as {segments?}).segments`
    (matching the new wire field name)
  * empty-buffer returns `{ segments: [] }`
  * mergeVideoSegments signature takes VideoSegment[]
  * createArchive consumes videoBufferResponse.segments
  * saveArchive log line says "segments"

Verification:
- npx tsc --noEmit clean.
- npx vitest run → 28 passed / 2 failed (the 2 fixture-empirical
  ffmpeg dry-runs; unchanged — they're gated on ./smoke.sh regen).
- npm run build succeeds, all 60 modules transformed.
- grep predicates clean in src/:
  * no addChunk / trimAged / firstChunkSaved / isFirst
  * no TransferredVideoChunk / VideoChunk (old names)
  * 21 occurrences of new names propagated correctly
- Pre-existing port-serialization.test.ts still GREEN: its `isFirst`
  references are inside inline-test-scoped objects (not imports
  from production types), and its tests assert JSON-roundtrip
  behaviour rather than the production type shape.
2026-05-15 21:15:19 +02:00
670daa3fe8 feat(fix-a3): adapt SW receive path to segment semantics
Follow-up to the D-13 activation in src/offscreen/recorder.ts. Each
entry on the BUFFER port message is now a self-contained WebM
segment (not a partial chunk), so the SW-side concat is trivial:
sort by timestamp and Blob-concatenate. The resulting multi-EBML-
header file plays natively in Chrome (SPEC §10 #7 scope).

Changes in src/background/index.ts:
- mergeVideoChunks renamed to mergeVideoSegments; doc comment and
  log lines say "segment" throughout. No behavioural change beyond
  removing the now-stale per-chunk `isFirst` logging.
- getVideoBufferFromOffscreen no longer reads or carries the
  `isFirst` field when decoding wire payload into VideoChunk —
  D-13's segment lifecycle makes the flag meaningless (every
  segment is a fresh recorder boundary, every segment's first
  chunk is implicitly the header). The field stays optional on
  VideoChunk for one more commit; commit 4 sweeps the type
  rename and drops it.
- The single mergeVideoChunks call site in createArchive updated
  to the new name.

Verification:
- npx vitest run → 28 passed / 2 failed (same 2 empirical-ffmpeg
  REDs from webm-playback.test.ts; unchanged from prior commit —
  the fixture is still the stale single-continuous-recorder one).
- npx tsc --noEmit clean.
- src/background/index.ts now has zero references to addChunk /
  trimAged / firstChunkSaved / isFirst.
2026-05-15 21:12:46 +02:00
6a1a034030 feat(fix-a3): activate D-13 restart-segments in src/offscreen/recorder.ts
Replaces the single-continuous-MediaRecorder + age-trim lifecycle
(D-09..D-11, retired) with the restart-segments lifecycle pre-staged
in CONTEXT.md D-13 and RESEARCH.md Pattern 3. Debug session
webm-playback-freeze proved empirically that the prior approach
orphans VP9 P-frame keyframe references when middle chunks are
trimmed — ffmpeg dry-run on last_30sec.webm produced 8 "Error
submitting packet to decoder" lines and the playback froze ~1 s in
Chrome (root-cause-confirmed in the debug session).

Architecture change:
- MediaRecorder is rotated every SEGMENT_DURATION_MS = 10_000 ms on
  the SAME mediaStream (so no new getDisplayMedia user gesture).
- Each segment is started fresh, so the encoder seeds an EBML header
  + initial VP9 keyframe — segment is independently decodable.
- Up to MAX_SEGMENTS = 3 finalized segments are retained (3 × 10 s =
  30 s, matching the legacy operator-facing window).
- MediaRecorder.start() runs without a timeslice — exactly one
  ondataavailable per .stop() yields exactly one Blob per segment.
- onstop → onSegmentStopped finalizes currentChunks, evicts oldest
  if over the cap, and immediately starts the next segment.
- ~50–200 ms recording gap at each rotation boundary is the
  documented trade-off per CONTEXT.md D-13.

API surface delta:
- REMOVED: addChunk, trimAged, getBuffer, firstChunkSaved
  (entire D-09..D-11 ring-buffer module retired)
- ADDED:   getSegments, pushSegmentForTest (test seam),
           MAX_SEGMENTS, SEGMENT_DURATION_MS exports
- KEPT:    assertCodecSupported, resetBuffer (semantics updated to
           clear segments + currentChunks + rotation timer),
           VIDEO_BUFFER_DURATION_MS (now derived as
           MAX_SEGMENTS * SEGMENT_DURATION_MS = 30 s)
- bootstrap(), port lifecycle, OFFSCREEN_READY handshake, T-1-04
  sender-id check — all unchanged.

encodeAndSendBuffer iterates segments + the in-flight currentChunks
(if non-empty) so SAVE_ARCHIVE seconds after START_RECORDING still
returns the partial first segment instead of an empty buffer. Each
segment is base64-encoded per the D-12 wire-format contract.

Verification:
- npx vitest run → 28 passed / 2 failed; the 2 failures are exactly
  the empirical ffmpeg dry-runs in tests/offscreen/webm-playback.test.ts
  which stay RED until the operator regenerates the committed fixture
  via ./smoke.sh (expected and documented).
- tests/offscreen/segment-keyframes.test.ts production-driven RED
  block is now GREEN — getSegments exists and meets the cap contract.
- tests/offscreen/segment-rotation.test.ts (8 tests, added in the
  prior commit) all GREEN.
- npx tsc --noEmit clean.
- Zero new `as any` / `@ts-ignore` introduced.
2026-05-15 21:11:07 +02:00
5530292270 feat(fix-a3): retire ring-buffer first-chunk pin tests, add segment-rotation contract
Per debug session webm-playback-freeze "Activation Plan" step 4: the
D-09..D-11 ring-buffer semantics (first-chunk header pin + 30 s age trim)
are being replaced by D-13 restart-segments. The pinned-header assertions
were architecture-specific and become meaningless once each segment is
a self-contained WebM with its own EBML header and seed keyframe.

Changes:
- tests/offscreen/ring-buffer.test.ts: collapsed to a single breadcrumb
  test pointing at the successor file. Kept the path so git history /
  failure bisects land on the retirement commit cleanly.
- tests/offscreen/segment-rotation.test.ts (new): 8 tests pinning the
  D-13 invariants against the production recorder module:
    * MAX_SEGMENTS = 3, SEGMENT_DURATION_MS = 10_000 (= legacy 30 s window)
    * empty-by-default, ordered, oldest-evicted-at-cap
    * resetBuffer clears
    * getSegments returns a defensive snapshot (no internal aliasing)
  Uses a `pushSegmentForTest` seam so vitest can drive rotation
  deterministically without instantiating a real MediaRecorder.

RED today by design (TDD discipline) — the segment-rotation suite
imports `getSegments`, `pushSegmentForTest`, `MAX_SEGMENTS`,
`SEGMENT_DURATION_MS` from src/offscreen/recorder.ts. Those exports
land in the next commit. tsconfig.include is "src" only so tsc stays
clean during the RED window.
2026-05-15 20:59:01 +02:00
bf076199b4 docs(fix-d12): resolve debug session and update STATE
- Mark .planning/debug/d12-blob-port-transfer-fails.md as
  status: resolved; fill in the Resolution section with the
  applied fix (5 commit hashes, files changed), verification
  output (15/15 tests, tsc clean, vite build green, zero
  as-any/ts-ignore in fix-touched files), and inline answers
  to the specialist-review questions raised by the planner.
  Move the file to .planning/debug/resolved/.
- Update STATE.md frontmatter (stopped_at) + Decisions log
  + Session Continuity to record the D-12 fix landing and
  the open Plan 07 ffprobe gate (still requires operator
  smoke.sh + ffprobe re-run before Phase 1 can close).
- Land smoke.sh — the operator's D-12 acceptance-gate harness
  that surfaced the original failure. Self-contained: dedicated
  /tmp/mokosh-smoke-profile, auto-accept desktop-capture picker,
  Downloads polling, ffprobe gate, fixture staging.

REQ-video-ring-buffer remains NOT-complete — Plan 07 owns it,
operator must re-run ./smoke.sh to verify the fix end-to-end
in Chrome.

Refs: debug session d12-blob-port-transfer-fails (resolved).
2026-05-15 20:23:29 +02:00
d5bb948d95 feat(fix-d12): decode chunks from base64 in SW BUFFER receive
- Read incoming port.chunks as TransferredVideoChunk[] (was
  VideoChunk[] — but that was a lie because Blob doesn't survive
  JSON serialization across the port boundary).
- Decode each wire chunk via base64ToBlob(wire.data, wire.type) and
  resolve VideoBufferResponse with the resulting VideoChunk[]. The
  existing mergeVideoChunks downstream sees real Blobs and produces
  a real WebM-prefixed merged blob.
- Defensive per-chunk decode: log + skip individual decode failures
  rather than blowing up the whole fetch. Falls back to
  video/webm;codecs=vp9 if the wire chunk somehow omits the type
  (defense-in-depth — the offscreen always populates it).
- Document the 2 s BUFFER_FETCH_TIMEOUT_MS budget: covers worst-case
  encode + post-message + JSON parse with > 1.5 s of headroom for
  the current 15-chunk × 100 KB sizing.

Refs: debug session d12-blob-port-transfer-fails, D-17 port lifecycle.
2026-05-15 20:18:31 +02:00
283184978f feat(fix-d12): encode chunks to base64 in offscreen REQUEST_BUFFER handler
- Convert each VideoChunk's Blob to a TransferredVideoChunk via
  blobToBase64 before keepalivePort.postMessage. JSON.stringify(blob)
  === "{}" across extension contexts, so the previous direct send of
  VideoChunk[] was silently corrupting binary content on the wire.
- Move the work into encodeAndSendBuffer() to keep onPortMessage
  synchronous-typed (chrome.runtime.Port.onMessage ignores return
  values; the listener stays void-returning, the async work is
  fire-and-forget).
- Defensive per-chunk encode: log + skip individual encoding failures
  rather than crashing the whole BUFFER response. Operators get partial
  video > no video.
- Re-check keepalivePort !== null AFTER the await: the port may have
  disconnected during encoding (~100 ms for 15 chunks of ~100 KB each
  per the d12 sizing estimate).

Refs: debug session d12-blob-port-transfer-fails, D-17 port lifecycle.
2026-05-15 20:16:41 +02:00
d653283bc4 feat(fix-d12): add TransferredVideoChunk wire-format type in src/shared/types.ts
- 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.
2026-05-15 20:07:40 +02:00
c0d9166a1d feat(fix-d12): add binary encode/decode helpers in src/shared/binary.ts
- 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.
2026-05-15 20:06:51 +02:00
1ebfb42b30 docs(01-06): complete vite.config.ts collapse plan
- 01-06-SUMMARY.md: detailed write-up — 226 → 21 lines, Outcome A
  reconciliation (dist/src/offscreen/index.html), full dist layout
  for Plan 07's smoke test, T-1-NEW-06-01 / T-1-NEW-06-02 grep gates
- STATE.md: completed_plans 5 → 6, percent 71 → 86, current plan
  advanced 6 → 7, two new decisions logged, session stopped_at updated
- ROADMAP.md: Phase 1 plan progress row 4/7 → 6/7; 01-06-PLAN.md
  checked off

REQ-video-ring-buffer remains unchecked — Plan 07 owns the ffprobe gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:17:43 +02:00
6aeeda495c fix(01-06): align ensureOffscreen URL with crxjs emit path
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>
2026-05-15 18:11:05 +02:00
23e69d0b77 refactor(01-06): delete inline copy-offscreen plugin and orphan offscreen/ directory
- 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>
2026-05-15 18:10:00 +02:00
9e236cbc7b docs(01-05): complete SW shrink + port host plan
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.
2026-05-15 18:07:07 +02:00
5cd1519858 feat(01-05): wire SW-side port host and port-based buffer fetch
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.
2026-05-15 18:02:51 +02:00
886376e789 refactor(01-05): delete legacy SW buffer, alarms, IndexedDB, tabCapture paths
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.
2026-05-15 17:59:53 +02:00
05d00509bf docs(01-04): complete offscreen port keepalive + OFFSCREEN_READY handshake plan
- Add 01-04-SUMMARY.md with TDD RED/GREEN/REFACTOR gate records,
  acceptance grep gates, threat mitigations (T-1-04, T-1-NEW-04-01),
  Plan 05 SW-side handoff (REQUIRED sender.id === chrome.runtime.id
  check), and 1 Rule 1 deviation documented
- Advance STATE.md Plan counter 4 → 5, progress 43% → 57%
- Append 3 decisions to STATE.md Accumulated Context
- Update ROADMAP.md: 01-04-PLAN checkbox → [x], phase progress row 3/7 → 4/7

REQ-video-ring-buffer NOT marked complete — still pending Plan 07
ffprobe D-12 gate per the requirement's traceability.
2026-05-15 17:54:04 +02:00
b0f4adcbd4 refactor(01-04): remove stale 'Plan 04 wires this' comments now that it's wired
- 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.
2026-05-15 17:47:32 +02:00
b064a214b2 feat(01-04): wire offscreen port keepalive and OFFSCREEN_READY handshake
- 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.
2026-05-15 17:46:33 +02:00
30e5efd364 docs(01-03): complete offscreen recorder TDD plan
- 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>
2026-05-15 17:42:21 +02:00
c5828d38ef feat(01-03): add OffscreenLogger and clean up shared types
- 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>
2026-05-15 17:37:58 +02:00
fff1aea592 feat(01-03): implement offscreen recorder ring buffer and codec strict-mode
- 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>
2026-05-15 17:34:00 +02:00
edc605d475 docs(01-02): complete wave-0 test infrastructure plan
- 01-02-SUMMARY.md created (Vitest 4.1.6 installed; 4 RED test files
  pinning Plans 03+04 contracts; tests/fixtures/.gitkeep marker)
- STATE.md advanced: plan 2/7 -> 3/7; progress 14% -> 29%; metric row
  added; 3 decisions logged; session continuity updated
- ROADMAP.md progress row updated: Phase 1 = 2/7 In Progress
- REQUIREMENTS.md: REVERTED premature [x] + "Complete" marking of
  REQ-video-ring-buffer (Plan 01-01 mistakenly marked it; the requirement
  is satisfied by Plans 03+04+07's implementation + ffprobe gate, not by
  RED test scaffolding). Now reads "[ ]" + "In Progress" — honest state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:28:49 +02:00
408aa3354c test(01-02): add RED handshake + port tests
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>
2026-05-15 17:25:03 +02:00
d7840a811c test(01-02): add RED codec-check tests
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>
2026-05-15 17:24:18 +02:00
2e73a21151 test(01-02): add RED ring-buffer tests
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>
2026-05-15 17:23:50 +02:00
57fa29e36b test(01-02): add vitest.config.ts
- Node-environment test runner (Blob shimmed via undici in Vitest 4+)
- Scoped include: tests/**/*.test.ts (production code never picked up)
- typecheck disabled — tsc --noEmit runs separately via npm run build
- No path aliases (tsconfig.json defines none; relative imports used)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:23:14 +02:00
ebf015aa0d test(01-02): wave-0 setup — install vitest
- 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>
2026-05-15 17:22:51 +02:00
13b67f5136 docs(01-01): complete doc-cascade plan
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
2026-05-15 17:19:48 +02:00
4a5194e15f docs(01-01): swap manifest permissions tabCapture->desktopCapture, drop alarms (D-A6/D-05)
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.
2026-05-15 17:16:28 +02:00
32bc99642e docs(01-01): amend Phase 1 description + Success Criterion #2 per D-A5
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.
2026-05-15 17:16:07 +02:00
597d967ccf docs(01-01): amend REQ-video-ring-buffer per D-A4
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.
2026-05-15 17:15:24 +02:00
b1ed2cbf49 docs(01-01): amend PROJECT.md DEC-003/DEC-010 + Constraints per D-A3
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.
2026-05-15 17:14:59 +02:00
fb88830d29 docs(01-01): retire 2 constraints + add CON-display-capture-binding per D-A2
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.
2026-05-15 17:14:15 +02:00
125c032656 docs(01-01): amend decisions.md DEC-003/DEC-010 per D-A1
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.
2026-05-15 17:13:39 +02:00
acb9033293 docs(01): record Phase 1 planning complete (7 plans, 7 waves)
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>
2026-05-15 17:09:09 +02:00
0811c6a292 docs(01-01): cite D-05 in must_haves per coverage gate .planning/phases/01-stabilize-video-pipeline/01-01-PLAN.md 2026-05-15 17:07:49 +02:00
576280f6aa docs(01): mark Open Questions RESOLVED in research per checker iteration 1
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>
2026-05-15 16:50:40 +02:00
519a0d8a99 docs(01): revise plan 07 wave + ffprobe verify guard per checker iteration 1
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>
2026-05-15 16:50:32 +02:00
61c3e03069 docs(01): revise plan 06 depends_on per checker iteration 1
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>
2026-05-15 16:50:26 +02:00
51890b0bc4 docs(01): revise plan 05 wave per checker iteration 1 cascade
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>
2026-05-15 16:50:19 +02:00