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 (6ac23fdRED,7645765GREEN,1baaf45A14 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>
11 KiB
slug, status, goal, trigger, tdd_mode, phase, plan, opened, orchestrator_diagnosed, red_commit, green_commit, a14_commit
| slug | status | goal | trigger | tdd_mode | phase | plan | opened | orchestrator_diagnosed | red_commit | green_commit | a14_commit |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-09-save-stops-recording | awaiting_human_verify | find_and_fix | Plan 01-13 Task 9 operator empirical UAT — clicking SAVE downloads archive but recording continues; toolbar press re-opens SAVE-only popup; operator confusion (4× toolbar presses → 2 zips downloaded, 1 misclick) | true | 01-stabilize-video-pipeline | 01-09 | 2026-05-19 | true | cd83eb0 |
4f4c3e2 |
2b6c24b |
Debug session 01-09-save-stops-recording — SAVE_ARCHIVE must auto-stop recording (SPEC one-shot intent)
Problem statement
During the Plan 01-13 Task 9 operator empirical UAT 2026-05-19, after the
operator clicked SAVE in the popup during an active recording session, the
extension produced the requested archive zip (downloaded to ~/Downloads)
but the recording did not stop: the toolbar badge stayed REC, Chrome's
"Sharing your screen" banner remained visible, and subsequent toolbar
presses re-opened the SAVE-only popup with no feedback indicating the
prior SAVE had succeeded. The operator performed 4 toolbar presses across
~94 seconds; 2 zips downloaded (session_report_2026-05-19_10-59-44.zip +
session_report_2026-05-19_11-01-18.zip); the SW console captured 2
SAVE_ARCHIVE receives + 2 Archive download started events, with the
recording stream alive throughout.
Root cause (orchestrator-diagnosed)
src/background/index.ts:744-748 SAVE_ARCHIVE handler invokes saveArchive(),
which produces the zip + calls chrome.downloads.download, then returns
{success: true} — but the handler never signals offscreen to stop the
recording. The recorder's mediaStream + MediaRecorder + rotation timer
all stay live. The operator's perception ("nothing happened") was correct:
the popup did not transition state after SAVE; the badge did not transition;
no notification fired. This violates the SPEC Тз расширение фаза1.md
"one click MUST produce a self-contained archive" — which implies one-shot
save-and-done semantics. The original design's "always-on" framing was
over-extended by engineering interpretation; the SPEC actually calls for
a deliberate stop after each save.
Fix design
Two-file patch matched to the existing parallel Bug B (user-stopped-sharing)
pattern in .planning/debug/resolved/01-09-recovery-flow.md:
src/background/index.ts SAVE_ARCHIVE handler
After downloadArchive returns (i.e. inside saveArchive() after
the download dispatch succeeds, before the function returns {success: true}):
// Plan 01-13 Task 9 operator UAT closure: SAVE = one-shot per SPEC intent.
// Send STOP_RECORDING to offscreen + transition state to IDLE so the
// operator sees a clean reset (badge clears, popup empties, sharing
// banner closes via track.stop()). Mirrors the Bug B user-stopped-sharing
// path: deliberate stop ≠ error; no recovery notification.
try {
await chrome.runtime.sendMessage({ type: 'STOP_RECORDING' });
} catch (sendErr) {
// Offscreen may be unreachable mid-teardown; non-fatal — SW-side
// state still transitions to IDLE so the operator regains the
// restart path.
logger.warn('STOP_RECORDING dispatch after save failed:', sendErr);
}
isRecording = false;
setIdleMode();
src/offscreen/recorder.ts STOP_RECORDING handler
ALREADY EXISTS at recorder.ts:848 — invokes stopRecording() which
correctly nulls mediaStream + stops the MediaRecorder + stops all tracks
- clears the rotation timer. No edit needed on the offscreen side.
Edge case decided: resetBuffer is intentionally NOT called by the SW
post-save because:
stopRecording()in recorder.ts does NOT call resetBuffer (line 559 comment: "we do NOT call resetBuffer() here — the operator may want to STOP and then SAVE the buffered footage").- After SAVE_ARCHIVE the buffer's contents have already been consumed
into the zip; the operator's next session will start fresh because
startRecording()calls resetBuffer at line 318. - Calling resetBuffer here would be defensive overkill — the segments are about to be overwritten on the next START anyway, and a no-op resetBuffer on an already-cleared-at-next-start buffer wastes nothing.
TDD plan
RED unit tests (NEW file: tests/background/save-archive-stops-recording.test.ts)
- After
SAVE_ARCHIVEreceive completes →chrome.action.setBadgeTextcalled withtext=''(setIdleMode side effect) - After
SAVE_ARCHIVEreceive →chrome.action.setPopupcalled withpopup=''(setIdleMode side effect — the popup-empties signal that re-enables onClicked restart path) - After
SAVE_ARCHIVEreceive →chrome.runtime.sendMessagecalled with{type:'STOP_RECORDING'}(the SW → offscreen STOP signal) - After
SAVE_ARCHIVEreceive → NO recovery notification fired (chrome.notifications.createnot called withmokosh-recovery-*id)
A14 harness extension
tests/uat/extension-page-harness.ts — add assertA14 after assertA13:
- Pre-condition: A13 just dispatched a SAVE_ARCHIVE; recording state after that save should now be IDLE (badge='', popup='').
- Reads
chrome.action.getBadgeText({})→ expects'' - Reads
chrome.action.getPopup({})→ expects''(relative path empty; Chrome returns empty string when setPopup was called with'') - Snapshots
chrome.notifications.getAll()keys → expects NO id withmokosh-recovery-prefix added since A13 (verifies no recovery notif fired on the SAVE auto-stop)
Recommended simplification per spec: skip direct isRecording query
(no bridge op exposes it). The badge proxy is sufficient — production
state machine pairs isRecording updates with badge transitions
atomically (no state-machine path desyncs them).
Files touched
tests/background/save-archive-stops-recording.test.ts(NEW)src/background/index.ts(saveArchive() function — append STOP signal- setIdleMode + isRecording=false)
tests/uat/extension-page-harness.ts(assertA14 + global surface)tests/uat/lib/harness-page-driver.ts(driveA14)tests/uat/harness.test.ts(orchestrator — add A14 in sequence)
Note: STOP_RECORDING is already in MessageType (src/shared/types.ts:14)
— no type-system surface change needed.
RED → GREEN evidence
- RED commit SHA:
cd83eb0(4 tests, 3 RED + 1 regression-guard GREEN) - GREEN commit SHA:
4f4c3e2(src/background/index.tssaveArchive finally block) - A14 commit SHA:
2b6c24b(harness extension + A13 amendment) - vitest count: 98/98 GREEN (was 94/94 baseline; +4 from
tests/background/save-archive-stops-recording.test.ts) npm run test:uat: 15/15 GREEN (was 14/14; +1 from A14)npx tsc --noEmit: exit 0npm run build: exit 0
Vitest output snippet (Tests A-D)
✓ A: SAVE_ARCHIVE triggers setBadgeText({text:""}) — recording cleared after save
✓ B: SAVE_ARCHIVE triggers setPopup({popup:""}) — onClicked path re-enabled
✓ C: SAVE_ARCHIVE dispatches STOP_RECORDING via chrome.runtime.sendMessage
✓ D: SAVE_ARCHIVE does NOT fire a mokosh-recovery-* notification
Harness A14 output snippet
A14 — post-SAVE auto-stop state: badge='' + popup='' + no new mokosh-recovery-* notif: PASS
[PASS] A14.1: badge text is '' after SAVE_ARCHIVE auto-stop (setIdleMode)
[PASS] A14.2: popup is '' after SAVE_ARCHIVE auto-stop (setIdleMode re-enables onClicked)
[PASS] A14.3: NO new mokosh-recovery-* notification (deliberate stop != error)
Harness A14 addition rationale
A14 verifies the fix end-to-end via real Chrome + real MediaRecorder. A13 amended to setupFreshRecording + 11s segment-settle before its own SAVE dispatch (A12's SAVE now auto-stops per the new fix). A14 itself is read-only: snapshots recovery-notif ids, settles 500ms, then asserts post-state badge='' + popup='' + recoveryDelta=0. No new SAVE dispatch in A14 — chains off A13's SAVE for the event under test. Minimizes harness wall-clock cost (~11s for A13 re-setup; ~500ms for A14 settle).
Direct isRecording proxy check skipped per orchestrator simpler-design recommendation: the production state machine pairs isRecording with badge transitions atomically; badge='' is a reliable proxy.
Noteworthy
-
No PortMessage / Message type changes were needed.
STOP_RECORDINGwas already insrc/shared/types.ts:14MessageType union (from Plan 01-05's offscreen rebuild). The offscreenSTOP_RECORDINGcase atsrc/offscreen/recorder.ts:848was also already wired (callsstopRecording()which correctly nulls mediaStream + stops tracks + clears rotation timer). The fix was purely a missing dispatch on the SW side — not a missing type or handler. -
resetBufferquestion resolved cleanly: NOT called from the SW post-save. Rationale documented in the debug record's "Fix design" section:stopRecording()deliberately does not call resetBuffer (recorder.ts:559 comment "operator may want to STOP then SAVE"); the next session'sstartRecording()calls resetBuffer at line 318 so the buffer is fresh anyway. Calling it from the SW would be defensive overkill. -
Empty-buffer-path trade-off documented inline in src/background/index.ts: the catch branch emits
RECORDING_ERROR{error:'empty-video-buffer'}which the SW's own onMessage handler routes through setErrorMode + creates amokosh-recovery-*notification. The new finally block then runs setIdleMode after — so the FINAL badge state is OFF but the recovery notif lingers visibly. The unit test mock forchrome.runtime.sendMessagedoesn't loop back to the SW's own onMessage (so Test D appears clean), but in production an empty-buffer SAVE WOULD briefly surface a recovery notification. This is acceptable UX — operator sees "something went wrong, click to restart" and the badge correctly resolves to OFF.
Follow-ups
- Orchestrator runs pre-checkpoint bundle gates (SW CSP-safety, Node-globals
in SW chunk, DOM-globals not in SW chunk, Tier-1 hook-string grep over
dist/) BEFORE re-surfacing operator UAT. Per
feedback-pre-checkpoint-bundle-gates.mdoperator time = automation cannot verify. - Operator UAT after this lands should confirm: SAVE click → zip lands + badge clears + sharing banner closes + popup empties + subsequent toolbar click starts a NEW recording session (clean state machine).
Follow-ups
- Orchestrator runs pre-checkpoint bundle gates (SW CSP-safety, Node-globals
in SW chunk, DOM-globals not in SW chunk, Tier-1 hook-string grep over
dist/) BEFORE re-surfacing operator UAT. Per
feedback-pre-checkpoint-bundle-gates.mdoperator time = automation cannot verify. - Operator UAT after this lands should confirm: SAVE click → zip lands + badge clears + sharing banner closes + popup empties + subsequent toolbar click starts a NEW recording session (clean state machine).
SUPERSEDED 2026-05-19 — Charter REVERSED
This fix was REVERSED on 2026-05-19 per operator UX iteration preferring the original "always-on safety net" charter. The new (= original-original) contract: SAVE creates a zip but does NOT stop the recorder; recording is continuous until the operator clicks Chrome's "Stop sharing" banner, the browser closes, or the extension is uninstalled.
See the new debug record: .planning/debug/resolved/01-09-save-does-not-stop-recording.md
and Plan 01-09 Amendment 3.
This record remains as audit trail for the charter cycle. The 4f4c3e2 fix
was technically correct against the (now-reversed) Amendment 2 charter;
the reversal is a UX preference, not a technical defect.