Operator-empirical Task 4 checkpoint flagged the dark-mode mark stroke
as muddy ink-on-madder. Root cause: .welcome-hero__mark used
`color: var(--mks-fg-inverse)`, which is a SEMANTIC text-foreground-on-
inverse-surface token that flips to ink-900 in the dark theme
(tokens.css line 244). The mark sits on a theme-independent madder-600
circle, so the stroke must be theme-independent too.
Fix: introduce a dedicated BRAND-COMPONENT token --mks-mark-stroke =
var(--mks-linen-50) in the universal :root block. CRUCIALLY NOT
overridden in the .dark/[data-theme="dark"] block — stays linen-50 on
every surface. Rewire .welcome-hero__mark to point at the new token.
SVG (mokosh-mark.svg) unchanged — `stroke="currentColor"` cascade
plumbing identical; only the wrapper's color source changed.
A35 strengthened: extracted live-DOM probe into a helper, now probes
BOTH light + dark themes (data-theme="dark" toggle on documentElement),
and added A35.5 — the decouple proof that light.computedStroke ===
dark.computedStroke === "rgb(250, 247, 241)" (linen-50). No new
__MOKOSH_UAT__ symbol; FORBIDDEN_HOOK_STRINGS stays at 12.
Scope expansion note: src/welcome/welcome.css was not in Plan 04-06
re-plan iter-2 files_modified. The edit is authorized by the operator's
TWEAK verdict on Task 4 checkpoint.
Verification:
- /tmp/04-06-welcome-hero-{light,dark}.png re-shot — both show identical
crisp linen-on-madder grid icon.
- A35.5 LIVE-DOM probe (UAT): light="rgb(250, 247, 241)", dark=same.
- UAT 36/36 GREEN; vitest 187 + 1 tolerated webm-remux flake.
- 6/6 pre-checkpoint bundle gates PASS; FORBIDDEN_HOOK_STRINGS = 12.
Debug session: .planning/debug/04-06-dark-mode-mark-decouple.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause: driveA33's A33.1 hard-gated on the chrome.runtime.sendMessage
SAVE_ARCHIVE callback ack. After the Puppeteer CDP worker.close() SW kill,
the SAVE_ARCHIVE message wakes a fresh SW instance; that instance runs the
multi-step saveArchive() pipeline (offscreen video-keepalive port
re-establishment + REQUEST_BUFFER round-trip + rrweb collection + zip
build). The harness's original sendMessage response port has its own MV3
lifetime — on a 5-min-aged SW the pipeline INTERMITTENTLY outruns it,
surfacing chrome.runtime.lastError "message port closed before a response
was received". The archive is still written correctly every time, which is
why A33.2/A33.3 always passed (Plan 04-05 full-mode UAT: A33.1 FAIL while
A33.2/A33.3 PASS at 1.56 MB). A33.1 was gating a CI assertion on a
best-effort transport ack with inherent MV3 non-determinism.
Fix (harness-side only, Option A — race-free reframe): A33.1 now gates on
the durable race-free signal — a fresh archive on disk — via the canonical
snapshotExistingZips + pollForNewOrUpdatedZip helpers (also used by
driveA12/A13/A27). The sendMessage ack is demoted to a soft non-gating
diagnostic. This is exactly the signal the proven-reliable spike already
uses. A33.2/A33.3 substantive checks are intact and now read the verified
fresh zip. No new symbol; FORBIDDEN_HOOK_STRINGS unchanged at 12. The SW
SAVE_ARCHIVE handler is a correct MV3 async pattern — no production change.
Verified: full-mode A33 (genuine 5-min idle) 3/3 GREEN; skip-mode UAT
35/35 GREEN; tsc + build:test exit 0; vitest 184/184.
Debug session: .planning/debug/a33-save-ack-race.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Session-2 (continuation of d614462 INCONCLUSIVE) executed disambiguation
plan and converged on a definitive verdict. Three independent observations
ruled out ALL architectural-failure hypotheses:
Step A: race-tolerant offscreen target attach (committed separately;
enabled visibility into the offscreen recorder + remux pipeline).
Step B: pre-kill and post-kill segment-count probes via the existing
`__mokoshOffscreenQuery 'get-segment-count'` bridge op (no new
test-only symbols introduced; FORBIDDEN_HOOK_STRINGS inventory
unchanged at 12 entries). Observed segments.length transition:
POST-PRIME=0 → PRE-KILL=3 → POST-KILL=3
Segments structurally survive the SW kill (offscreen still responds
to bridge query post-kill). Hypothesis A (architectural RAM loss
across SW termination) REFUTED.
Step C: SPIKE_SKIP_SW_KILL=1 env-var mode skips worker.close(). The
resulting videoSize is IDENTICAL to the canonical run (8505 bytes).
Hypothesis C (CDP-induced offscreen collateral teardown) REFUTED.
Since SW was not killed, its console listener stayed connected,
exposing the full Remux pipeline output:
[SW:Remux] Segment ts=1: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Segment ts=2: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Segment ts=3: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Remux complete: 0 frames, total timeline=0ms, output=8505 bytes
Each segment Blob has a valid track header (PixelWidth/Height parsed
successfully) but ZERO VP9 frames. Hypothesis B (canvas-captureStream
throttling in headless idle) CONFIRMED.
VERDICT: REFUTED-architecture (canvas-captureStream issue).
The architecture (offscreen-RAM `segments: Blob[] = []`) works
correctly; the spike's test methodology is invalid. The
`installFakeDisplayMedia` synthetic stream (canvas.captureStream(30)
on a hidden -9999px-offset 320x180 canvas) cannot sustain frame
production during a 5-min headless idle window despite the
`setInterval(drawFrame, 33ms)` belt-and-suspenders mitigation. This
matches the documented Chromium throttling of MediaRecorder on
invisible-canvas sources (Chrome bug 653548; auto-throttled-screen-capture
design doc; sendrec.eu blog "Why Canvas Breaks Your Screen Recorder").
ROUTING RECOMMENDATION (out of scope for this debug session):
- Do NOT proceed with the IndexedDB persistence plan-fix proposed by
Plan 04-04 SUMMARY. The plan-fix would NOT close SC #1 because the
spike would STILL produce 8505 bytes after IDB lands — the failure
is in the test's fake stream, not in segment persistence.
- Open a new plan slot (likely Plan 04-08 or a Phase 5 plan) that
reframes SC #1 verification methodology. Options:
(a) real getDisplayMedia in non-headless Puppeteer with
--auto-select-desktop-capture-source;
(b) video-file-backed MediaStream source (HTMLVideoElement
playing a bundled WebM) — bypasses canvas-captureStream
throttling entirely;
(c) reduce SC #1 wall-clock idle threshold to a value short
enough that canvas-captureStream survives (e.g., 30s) AND
add a separate manual operator-empirical test for 5-min.
ROADMAP SC #1 status: REMAINS OPEN. The architecture is sound; the
empirical verification gate is broken. Plan 04-04 SUMMARY's
characterization ("spike FAILED → architectural plan-fix needed") is
TECHNICALLY CORRECT on the first clause but INCORRECT on the second —
the spike's failure mode is in test infrastructure, not in production
code.
Files in this commit:
- tests/uat/spike-a33-sw-persistence.ts: added probeSegmentCount
helper using existing __mokoshOffscreenQuery bridge op; 3
checkpoints (POST-PRIME / PRE-KILL / POST-KILL); SPIKE_SKIP_SW_KILL=1
env-var skips worker.close() for Step C disambiguation.
- .planning/debug/sw-offscreen-persistence-investigation-session-2.md:
NEW session-2 debug note documenting full evidence trail + verdict
derivation + routing recommendation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-commit-ceremony verification of Plan 04-04 Wave 0 SPIKE finding
(videoSize=8505 bytes after 5-min SW idle + Puppeteer worker.close()).
Reproducibility: 4/4 runs (incl. prior 3726eee) produced identical
8505-byte WebM. Deterministic.
Chrome docs research: chrome.offscreen DISPLAY_MEDIA reason has NO
lifetime limit; offscreen "may outlive" its SW; Puppeteer #9995 +
crbug 1371432 document CDP attach distorting SW lifecycle; chromium
auto-throttled-screen-capture + Chrome Bug 653548 document canvas-
captureStream throttling on invisible/background tabs.
Verdict: INCONCLUSIVE — the spike's 8505-byte result is consistent
with THREE competing root causes (test-invalid headless throttling;
CDP-artifact collateral teardown; architectural offscreen-RAM-loss)
and the spike cannot disambiguate between them. Observability gaps:
launch.ts:225 filters offscreen console on background_page (MV2)
when MV3 offscreen is type 'page' → zero offscreen logs in all spike
runs.
Recommendation: PAUSE the ~2-4h IndexedDB plan-fix. Three cheap
disambiguation steps (~75 min total) can isolate the actual root
cause before committing. Detailed in the debug note's
routing_recommendation block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy chrome.tabs.query({ active: true, currentWindow: true }) +
"No active tab found" validation inside startVideoCapture were load-
bearing in the pre-D-01 chrome.tabCapture era but became functionally
dead after Plan 01-09's D-01 conversion to getDisplayMedia-in-offscreen.
The only post-D-01 consumer was a log line at index.ts:521.
The dead validation caused an activeTab-permission-scope asymmetry
between callers: chrome.action.onClicked grants activeTab on the click
gesture (so tab.url was readable → toolbar path worked silently) but
chrome.notifications.onClicked does NOT grant activeTab and the extension
has no `tabs` permission, so notifications.onClicked → startVideoCapture
threw "No active tab found" before reaching ensureOffscreen. Operator
2026-05-20 UAT against the new notifStartupCta CTA copy ("Mokosh ready.
Click to start a recording.", commit 4bba679) surfaced the silent
notification failure.
Surgical fix: remove the dead tab query + validation + tab-dependent log
(src/background/index.ts:514-521); replace with a tab-independent log
that documents WHY (cites D-01 + this debug session). captureScreenshot
+ saveArchive retain their genuine tab dependencies (tab.windowId for
chrome.tabs.captureVisibleTab; tab.id for content-script sendMessage).
Tests: tests/background/start-video-capture-no-tab.test.ts (NEW) pins
the contract with 3 cases (tabs.query → []; → [{id}] url-less; →
[{id,url,windowId}] regression guard for toolbar path).
Gates: vitest 153/153 GREEN (was 150/150 baseline; +3); test:uat 24/24
GREEN; tsc clean; build clean. Pre-checkpoint bundle gates per
feedback-pre-checkpoint-bundle-gates.md: SW chunk hook-string Tier-1
grep 0 matches; eval/Node-global/DOM-global matches unchanged from
baseline (all vendor-library feature-detect, guarded; no new imports).
Debug record: .planning/debug/resolved/01-09-notification-start-no-active-tab.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The build-completes Tier-1 gate at tests/background/no-test-hooks-in-prod-bundle.test.ts:247
was racing vitest's default 5000ms it() ceiling. Plan 01-10 closure shipped the welcome
page (commits d48a715 welcome mark + 49f087f welcome HTML/CSS/JS + 8 WOFF2 fonts) which
slowed standalone `npm run build` from ~2.88s to ~5.28s. The exec-level
BUILD_TIMEOUT_MS = 60_000 child-process bound was correctly declared at line 240, but
the surrounding it() block had no timeout option, so the 5s default fired first and the
60s exec bound was never reachable.
Surgical fix: add `, 30_000` 3rd arg to the it() call. 30s is ~6× the observed build
duration and well below the 60s exec ceiling, so both bounds remain meaningfully
active. SKIP_BUILD=1 env-var escape hatch untouched.
Acceptance gates:
- `npm test` (FULL, no SKIP_BUILD=1): 150/150 GREEN, exit 0
- `npx tsc --noEmit`: exit 0
- `npm run build`: exit 0
- Tier-1 grep gate: PASS (all 12 FORBIDDEN_HOOK_STRINGS asserted against dist/)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20 per debug
session 01-10-welcome-page-missing-mark). Closes the planning-coverage
gap where Plan 01-12 path-B (canonical tokens import) ran ahead of
01-10, leaving the welcome hero with a text placeholder 'Mokosh'
inside the rec-bg circle instead of the canonical 2×2 woven-square
mark from src/shared/brand/mokosh-mark.svg.
Why Option B (Vite ?url import) over manual WAR (A) or inline SVG (C):
- @crxjs/vite-plugin ^2.0.0-beta.25 auto-WARs transitively-reachable
resources from extension pages — no manifest.json edit needed.
- Vite default-inlines small SVGs (~600 bytes < 4096 byte default
assetsInlineLimit) as data:image/svg+xml URLs in the welcome chunk
— no extra HTTP request, no extra WAR entry.
- Hashed asset fallback works automatically if the SVG grows past
the inline limit in future revisions.
- Existing font-bundling precedent (dist/assets/Lora-*.woff2 +
IBMPlex*.woff2) proves the Vite + crxjs pipeline.
Files modified:
- src/welcome/welcome.ts — added markUrl import + populateMark() that
walks [data-mokosh-slot='mark'] and injects an <img>.
- src/welcome/welcome.html — added explanatory comment block; preserved
the data-mokosh-slot wrapper for forward-compat (the placeholder
span remains as the JS-fail-gracefully fallback).
- src/welcome/welcome.css — added .welcome-hero__mark-img rule
(60% sizing inside the existing styled circle wrapper).
- src/welcome/copy.ts — added 'welcome.hero.mark.alt' COPY key
(Russian per D-03 Sober voice).
- globals.d.ts — added *.svg?url ambient module declaration
(Vite recommended pattern; keeps tsconfig.json types: ['chrome']
clean by not requiring vite/client triple-slash directives).
- tests/uat/extension-page-harness.ts — extended A17 with A17.8
sub-check verifying the canonical mark SVG is bundled into the
welcome chunk (data URL OR file URL form) AND that the canonical
viewBox='0 0 32 32' is preserved through bundling.
Acceptance gates passed:
- npx tsc --noEmit exit 0
- npm run build exit 0
- SKIP_BUILD=1 npm test → 150/150 GREEN
- npm run test:uat → 24/24 GREEN including A17.8
- Tier-1 hook-string grep gate PASS (no FORBIDDEN_HOOK_STRINGS
in production bundle).
- Manifest valid JSON; web_accessible_resources auto-bundled.
- Pre-checkpoint bundle gates 1/2/3: vendor pre-existing hits
(JSZip + ts-ebml) confirmed identical pre-change via git stash
baseline; not caused by this fix.
Forward-looking deferred (out of scope):
- Issue 2 dark-surface contrast (e.g. chrome.notifications icon128
may need a light-stroke variant). The welcome hero's rec-orange
BG already provides high contrast with the dark ink stroke — this
is correct design. Per the orchestrator's explicit constraint,
light-variant mark for dark notification panels is deferred to
Phase 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator UAT 2026-05-20 rejected the build because the OS notification fired
on `chrome.runtime.onStartup` ("Recording started. I'm watching the last 30
seconds.") implied recording had auto-started when in fact recording was
not running. Per Phase 1 always-on charter recording does NOT auto-start;
the notification is the gesture surface that invites the operator to start
one (notifications.onClicked → startVideoCapture, src/background/index.ts:1038).
Root cause: a single i18n key `notifStartup` conflated the pre-recording
CTA-with-gesture path (the only path actually wired today) and a future
post-manual-start confirmation path. The key's own `.description` field
acknowledged the conflation. Operator-facing text leaned toward the
confirmation phrasing.
Fix (key split, no behavior change):
- `notifStartupCta` — EN: "Mokosh ready. Click to start a recording." /
RU: "Mokosh готов. Нажмите, чтобы начать запись." — wired into the
onStartup handler.
- `notifRecordingStarted` — preserves the original text ("Recording
started. I'm watching the last 30 seconds." / "Запись запущена…") for
a future post-manual-start confirmation flow.
- Fallback constant renamed `NOTIF_STARTUP_FALLBACK` →
`NOTIF_STARTUP_CTA_FALLBACK`; value updated to match the new CTA text.
- Inline test comment in tests/background/onstartup-notification.test.ts
refreshed to reference the new key + fallback. Assertion regex
/recording|recor|click/i covers both fallback + resolved locale variants,
no logic change.
Notification behavior preserved: same id prefix `mokosh-startup-`, same
priority, same icon, same onClicked → startVideoCapture wiring. No new
test-mode symbols (FORBIDDEN_HOOK_STRINGS inventory stays at 12).
Files modified:
- _locales/en/messages.json
- _locales/ru/messages.json
- src/background/index.ts
- tests/background/onstartup-notification.test.ts
Verification:
- npx vitest run --exclude tests/build/** --exclude tests/background/no-test-hooks-in-prod-bundle.test.ts: 104/104 GREEN
- npx vitest run tests/i18n/ tests/background/onstartup-notification.test.ts: 18/18 GREEN (locale-parity 4/4 + onstartup-notification 14/14)
- npx tsc --noEmit clean on src/background/index.ts
The 2 build-dependent vitest gates (tests/build/no-remote-fonts.test.ts +
tests/background/no-test-hooks-in-prod-bundle.test.ts) and npm run test:uat
are deferred to orchestrator-level re-verification after the parallel
Plan 01-10 mark-bundling fix also lands (operator-UAT re-spawn coordinated
by orchestrator).
Debug record: .planning/debug/resolved/01-09-startup-notification-misleading-text.md
Operator UAT rejection event: 2026-05-20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 01-09 Amendment 3 (2026-05-19) — atomic documentation pass for
the save-does-not-stop-recording charter reversal.
Changes:
- .planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md:
Amendment 3 block added above <success_criteria> (mirrors
Amendment 2 placement). Describes the reversed charter,
references the new debug record, points at the inverted
test file + harness A14.
- .planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md:
"Subsequent Reversal (2026-05-19)" footer added. Notes that
npm run test:uat still 15/15 GREEN under the inverted A14
contract; vitest baseline preserved at 98 GREEN.
- .planning/STATE.md:
Plan 01-13 closure block extended with CHARTER REVERSAL bullet
citing the 4 commit SHAs (6ac23fd RED, 7645765 GREEN,
1baaf45 A14 invert, this commit docs).
- .planning/debug/resolved/01-09-save-stops-recording.md:
SUPERSEDED 2026-05-19 footer appended (audit trail; original
fix was technically correct against its charter, reversal is
UX iteration not technical defect).
- .planning/debug/resolved/01-09-save-does-not-stop-recording.md:
NEW debug record landed directly in resolved/ (no checkpoint
cycle — orchestrator-diagnosed reversal). Documents symptom,
charter clarification cycle, fix shape, RED→GREEN evidence
with commit SHAs + vitest/harness output, anti-regression
coverage at unit + E2E layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 01-13 Task 9 operator UAT closure. Operator 2026-05-19 empirical
session: SAVE click downloaded zip but recording stayed live (badge=REC,
sharing banner persisted, subsequent toolbar press re-opened SAVE-only
popup). Operator pressed 4×, got 2 zips + confusion.
Root cause: src/background/index.ts saveArchive() returns success after
chrome.downloads.download without signaling offscreen to stop or
transitioning the SW state machine — SPEC `Тз расширение фаза1.md`
"one click MUST produce a self-contained archive" was over-extended to
"always-on" framing by the implementation.
Fix contract (RED today; GREEN after src/background/index.ts patch):
A: setBadgeText({text:''}) called post-save (setIdleMode side effect)
B: setPopup({popup:''}) called post-save (re-enables chrome.action.onClicked
restart path per MV3 contract)
C: chrome.runtime.sendMessage({type:'STOP_RECORDING'}) dispatched
(offscreen recorder.ts:848 STOP_RECORDING case already wired —
no offscreen-side change needed)
D: NO mokosh-recovery-* notification fires (deliberate stop ≠ error;
mirrors Bug B `user-stopped-sharing` suppression branch from
.planning/debug/resolved/01-09-recovery-flow.md)
Tests A/B/C RED (assertion errors `expected 0 >= 1`); Test D GREEN today
as the regression guard against fix over-rotating to setErrorMode.
Test architecture mirrors tests/background/request-id-protocol.test.ts:
synthetic BUFFER response delivered via port.onMessage listeners to drive
saveArchive's request-id'd buffer fetch to completion. Empty-segments
BUFFER causes createArchive → EmptyVideoBufferError → catch branch; the
fix's STOP+IDLE dispatch MUST happen on both success and empty-buffer
paths (operator UI contract: SAVE click = stop, success or empty alike).
Debug record: .planning/debug/01-09-save-stops-recording.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Untracked file present at session spawn (per orchestrator pre-flight
intelligence). The Bug B debug investigation that produced commit
b9eeeeb (the conditional-routing fix in src/background/index.ts
RECORDING_ERROR handler that Plan 01-11 assertion 6 verifies) was
recorded under .planning/debug/resolved/01-09-recovery-flow.md but
never committed. Importing it now so the debug provenance is
preserved alongside Plan 01-11's harness coverage of the bug class.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 01-08 fix took TWO iterations, not one. Iteration 1
(commits 52c7636 + 74400ae, archived in cc6e81a) resolved
the SW INIT crash via resolve.alias for ebml + chrome.* mock
for the Tier-1 Layer 1 gate. That landing masked a SECOND
defect — ts-ebml's EBMLDecoder constructor crashes with
`ReferenceError: Buffer is not defined` because MV3 SW has
no Buffer global. The runtime path is unreachable at module
init (EBMLDecoder is only constructed when remuxSegments
runs, which only fires from the SAVE_ARCHIVE handler), so
Layer 1 of the gate could not catch it.
Iteration 2 (commits dd7bf00 + 761dfc0) closed that gap by
extending the Tier-1 gate to Layer 2 (source-imports
webm-remux.ts, invokes remuxSegments — caught the Buffer
bug empirically) and applying B+ — vite-plugin-node-polyfills
with narrow Buffer-only config — to provide Buffer at SW
runtime via bundler-level import rewrite.
Updates to the debug archive:
- frontmatter `updated:` bumped to 12:25Z
- two new Evidence entries (12:15Z Layer 2 RED, 12:20Z B+
GREEN) document the iteration-2 empirical path
- one new Eliminated entry: "C-config alone is sufficient" —
FALSIFIED by Layer 2 (the resolve.alias fix from iteration
1 is necessary but not sufficient; ts-ebml's runtime Buffer
use is an orthogonal concern that requires the polyfill)
- Resolution.root_cause rewritten to describe BOTH defects
(bundler-config + runtime-Buffer) and explain why they
surfaced sequentially
- Resolution.fix rewritten with iteration-1 / iteration-2
structure, citing all 4 commits across both iterations
- Resolution.verification rewritten with explicit Layer 1
vs Layer 2 verification claims and the full vitest count
(62 passing, 2 failing — pre-existing fixture-dependent
webm-playback duration tests, unchanged)
- Resolution.files_changed lists all 4 commits across both
iterations + this archive update
The session was correctly resolved-and-archived after
iteration 1 with the information then available; iteration
2 is an additive correction once the extended gate surfaced
the second defect. Per the project's
feedback-pre-checkpoint-bundle-gates memory, the extended
Tier-1 gate is now the canonical bundle-loadability check
any future plan executor with SW surfaces must run before
operator-empirical checkpoints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests/background/sw-bundle-import.test.ts that loads the built SW
chunk under SW-simulated globals (Buffer/process/window/document stripped)
via a spawned Node child process. Pins the orchestrator-side gap that
caused Plan 01-08's SW init crash: the prior deps test only checked
SOURCE packages under default Node globals, never the bundled output, so
Vite/Rollup's CJS-interop bug (tree-shaking the `ebml` package while
leaving a dangling `{tools:f}=Pc` destructure against an empty Pc) went
undetected until operator empirical smoke.
RED against HEAD aabbd0c — failure surfaces the exact production error
("Cannot read properties of undefined (reading 'readVint')"), proving
the test is a true regression gate, not a tautology.
Also rewrites .planning/debug/01-08-sw-incompatibility.md to reflect the
actual root cause (Vite/Rollup CJS interop) rather than the orchestrator's
initial falsified hypothesis (new Function + Buffer globals — disproven
by Node simulation showing the throw fires at module-init line 12:33809
before any CSP-eval or Buffer-ref code path executes).
Full vitest: 60 passing + 3 RED (this gate + the 2 pre-existing Task 5
fixture-dependent duration tests). No regressions.
Per feedback-pre-checkpoint-bundle-gates.md (auto-loaded memory): any
future plan executor whose work surfaces a SW must run this test before
any operator-empirical checkpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
REQ-video-ring-buffer flipped from [x] back to [ ]. ROADMAP.md Phase 1
row reverted from [x] Closed 2026-05-15 to [ ] reopened 2026-05-16.
STATE.md status flipped phase_complete → phase_reopened with full
historical narrative preserved.
Root cause (confirmed at byte level by gsd-debugger 2026-05-16):
D-13's concat-of-self-contained-WebM-segments architecture produces a
3-EBML-header WebM that standards-compliant Matroska parsers
(mpv, ffmpeg, Chrome HTMLMediaElement) play only as the first segment
(~9.94 s) and silently drop the remaining 2 segments. Confirmed via
operator mpv drag-drop test of BOTH the canonical 2026-05-15 closure
fixture and the 2026-05-16 UAT-produced fixture — both exhibit the
same broken playback.
The 2026-05-15 "operator-confirmed clean Chrome playback" assessment
was insufficient: it verified the file plays without freezing but did
not measure total duration. Phase 1's primary deliverable
(REQ-video-ring-buffer / SPEC §10 #7) is therefore NOT satisfied.
Fix path chosen by user: ts-ebml (parse) + webm-muxer (write) to
replace mergeVideoSegments file-concat with real single-EBML remux.
Will land as Plan 01-08 via fresh /gsd-plan-phase ceremony.
RED test landed in tests/offscreen/webm-playback.test.ts (2 new
assertions on container-format-duration + ffmpeg-full-decode-duration).
2 failures, 53 baseline tests still GREEN.
Option C port-lifecycle refactor (debug session
empty-archive-port-race, commits 674c415..f0871c0) DID land cleanly
and is retained — that fix was orthogonal and correctly resolved the
silent-empty-archive symptom that previously masked this deeper bug.
Debug session: .planning/debug/d13-multi-ebml-concat-unplayable.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two doc updates closing the debug session per the resolved pattern this
phase has established (cf. resolved/d12-blob-port-transfer-fails.md and
resolved/webm-playback-freeze.md):
1. **Move debug session to resolved/** with the Resolution section
filled in (root_cause, fix, verification, files_changed). Status
flipped tdd_red_confirmed -> resolved. Original investigation
notes + bisect results + Option C strategy spec all preserved
in-place — the file is the full provenance trail.
2. **Amend 01-CONTEXT.md D-17** with the new port lifecycle commitments.
Append-only (D-17 itself untouched) per the doc cascade rule
established earlier this phase ("amendments append, do not replace,
to preserve SPEC provenance"). The amendment narrates:
- What was Claude's-discretion at Phase 1 plan time has been
specified by Option C.
- The 290 s pre-emptive setTimeout reconnect (Pitfall 4) is RETIRED.
- The architectural commitments added: PING/PONG health probe,
request-id'd REQUEST_BUFFER/BUFFER, SW retry on port replacement,
outer 10 s hard-timeout, operator-visible EmptyVideoBufferError
surface.
- The 4 pinning contracts added (port-health-probe,
request-id-protocol, port-lifecycle-continuous, plus the
refactored port-reconnect-race).
Suite remains 11 files / 53 tests, all GREEN. Quality gates intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 UAT Test 3 surfaced a two-headed BLOCKER:
(a) silent empty-video archive when save crosses a port-reconnect window,
(b) 3x "Attempting to use a disconnected port object" Uncaught Errors
starting at the 290 s pre-emptive reconnect mark.
Bisect confirmed: H1 (port lifecycle race) was introduced by Plan 01-04
(b064a21); H2 (createArchive silent-skip on empty segments) is an upstream
defect (555eb05) that became fatal once CR-01 + sweep #5 guaranteed the
silent-skip branch would fire on every save during a reconnect window.
This commit lands the 3 RED tests at the unit-test level — they match the
UAT error string byte-for-byte for H1/H1.b and pin the silent-drop
contract for H2. They will flip GREEN as the Option C architectural
refactor (request-id'd port protocol + port-health probe + retry +
operator-visible error surface) lands across the next commits.
Baseline: 8 files / 43 tests (40 GREEN, 3 RED). tsc --noEmit exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
- 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).