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>
11 KiB
slug, status, goal, trigger, created, updated, phase, plan, orchestrator_diagnosed, red_test
| slug | status | goal | trigger | created | updated | phase | plan | orchestrator_diagnosed | red_test |
|---|---|---|---|---|---|---|---|---|---|
| 01-09-notification-start-no-active-tab | awaiting_human_verify | find_and_fix | Operator UAT 2026-05-20: clicking onStartup notification ('Mokosh ready. Click to start a recording.') logs `Failed to start video capture: Error: No active tab found` and `notification-triggered start failed: Error: No active tab found` from the SW chunk (La function). Notification path silently fails; toolbar path unaffected. | 2026-05-20 | 2026-05-20 | 01-stabilize-video-pipeline | 01-09 | true | tests/background/start-video-capture-no-tab.test.ts |
Debug session 01-09-notification-start-no-active-tab — startVideoCapture must not depend on an active tab (D-01 cleanup gap)
Problem statement
After Plan 01-10 + the 2026-05-20 commit 4bba679 split the onStartup
notification CTA text to "Mokosh ready. Click to start a recording.",
operator UAT exercised the notification click path for the first time and
observed silent failure: the SW devtools console logged
[SW:Main] 2026-05-20T08:56:43.336Z Failed to start video capture: Error: No active tab found
at La (index.ts-CKI4Evvn.js:17:95466)
[SW:Main] 2026-05-20T08:56:43.336Z notification-triggered start failed: Error: No active tab found
at La (index.ts-CKI4Evvn.js:17:95466)
Badge stayed IDLE; no Chrome "Sharing your screen" banner; no recording began. Toolbar click path remained functional.
Root cause
src/background/index.ts:514-521 (pre-fix) inside startVideoCapture():
// Получаем активную вкладку
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab.id || !tab.url) {
throw new Error('No active tab found');
}
logger.log(`Starting video capture for tab ${tab.id}: ${tab.url}`);
This block was load-bearing in the pre-D-01 chrome.tabCapture era —
the capture stream-id was bound to the active tab. Since Plan 01-09's
D-01 conversion (chrome.tabCapture → getDisplayMedia whole-desktop in
offscreen), the tab reference has been functionally dead: every line
after 521 (ensureOffscreen(), await offscreenReady,
chrome.runtime.sendMessage({ type: 'START_RECORDING' }), isRecording = true, setRecordingMode(), error handling) makes no reference to tab
or to any tab property.
The two callers diverge because of the chrome permission model:
chrome.action.onClicked(toolbar) — Chrome grantsactiveTabon the click gesture;tab.urlis readable; the validation at line 517 passes; the dead log line executes; the function proceeds normally.chrome.notifications.onClicked— NOT a user gesture toward a tab.activeTabis not granted, and the extension manifest declares notabspermission (per.planning/intel/01-11-SUMMARY.mdfollow-up backlog: "tabs permission gap").chrome.tabs.query({ active: true, currentWindow: true })resolves to a tab whoseurlis undefined (or[]depending on Chrome version + window state);!tab.urlevaluates true; the function throws "No active tab found" beforeensureOffscreen()is reached.
The bug was latent because Plan 01-09's smoke validation only
exercised toolbar.onClicked. The new Plan 01-10 + debug-2-of-3
notification CTA text (commit 4bba679) explicitly invites operators
down the previously-untested notification.onClicked path. Operators
hit this on every browser-restart-after-install.
Fix
Surgical removal of the dead tab query + validation + tab-dependent
log from startVideoCapture (src/background/index.ts:514-521).
Replace with a tab-independent log message that documents WHY the
removal happened (D-01 cleanup gap + this debug session).
// Plan 01-09 D-01 cleanup gap (debug session
// `01-09-notification-start-no-active-tab`, 2026-05-20):
// The legacy chrome.tabs.query({ active: true, currentWindow: true })
// here was load-bearing in the pre-D-01 chrome.tabCapture era but
// is functionally dead post-D-01 — capture is whole-desktop via
// getDisplayMedia in offscreen and the SW-side start path needs
// no tab reference. The query also failed for chrome.notifications
// .onClicked callers (no activeTab grant + no `tabs` permission →
// tab.url undefined → "No active tab found" throw) so the onStartup
// notification CTA was silently broken. captureScreenshot +
// saveArchive retain their own genuine tab queries (tab.windowId
// for captureVisibleTab, tab.id for content-script sendMessage).
logger.log('Starting video capture (whole-desktop via getDisplayMedia in offscreen per D-01)');
Out of scope (NOT touched — genuine tab dependencies)
captureScreenshot()src/background/index.ts:568-591— passestab.windowIdtochrome.tabs.captureVisibleTab(tab.windowId, ...). Genuine dependency on the active tab's window.saveArchive()src/background/index.ts:741+— usestab.idforchrome.tabs.sendMessage(tab.id, ...)to query the rrweb content script for events, and uses the tab's window for the embedded screenshot. Genuine dependency.
These remain because (a) they only fire under operator gestures that
DO grant activeTab (toolbar click → popup → SAVE), and (b) they have
real downstream API consumers of tab fields. A separate Phase 5
"adopt tabs permission" decision is the appropriate forum for
hardening; not in scope for this surgical bug fix.
TDD evidence
RED test (new file)
tests/background/start-video-capture-no-tab.test.ts — 3 tests:
- A:
tabs.querymocked to[](no-activeTab notifications.onClicked context) → assertchrome.runtime.sendMessage({type:'START_RECORDING'})was dispatched (was failing pre-fix with "expected 0 to be greater than or equal to 1" — startVideoCapture threw before reaching sendMessage). - B:
tabs.querymocked to[{id: 1}](url-less tab) → same assertion. Was failing pre-fix for the identical reason. - C: REGRESSION GUARD —
tabs.querymocked to a fully-populated tab (action.onClicked / activeTab-granted context) → same assertion. Was passing pre-fix; MUST stay passing after the fix so we don't over-trim and break the toolbar path.
Pre-fix run:
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)
Post-fix run:
Test Files 1 passed (1)
Tests 3 passed (3)
Acceptance gates (all GREEN)
| Gate | Pre-fix baseline | Post-fix | Notes |
|---|---|---|---|
npm test |
150/150 | 153/153 | +3 from new test file; 27 → 28 test files |
npm run test:uat |
24/24 | 24/24 | No UAT changes; harness unaffected |
npx tsc --noEmit |
exit 0 | exit 0 | No type-system surface change |
npm run build |
clean | clean | Built in 23.36s |
Note: A single ffprobe-driven test (`tests/background/webm-remux.test.ts
ffprobe -count_frames`) showed an intermittent 5000ms timeout in one of the multi-suite runs but passed in isolation (5/5) and on the second full-suite run (153/153). This is a pre-existing flake under load (ffprobe child-process spawn time), entirely unrelated to this fix — no source files this fix touches are consumed by webm-remux tests.
Pre-checkpoint bundle gates (per saved memory feedback-pre-checkpoint-bundle-gates.md)
Inspected the production SW chunk dist/assets/index.ts-DBpA3-1k.js
(referenced by dist/service-worker-loader.js):
- Hook-string Tier-1 grep: 0 matches (TEST_HOOK / MOKOSH_TEST_HOOK
/
__test__/window.mokosh/globalThis.mokoshetc). - SW CSP safety (eval / new Function): 1 match — vendor library
internal templater (rrweb-adjacent), guarded by
typeofchecks; pre-existing in baseline build; unrelated to this fix (no new vendor imports). - Node-globals (require / process / Buffer / __dirname / global): 2
matches — all inside vendor library
ArrayBuffer/Bufferruntime feature-detect paths; pre-existing; not introduced by this fix. - DOM-globals (window / document / localStorage / alert / navigator):
3 matches — all inside vendor library
typeof window<"u"&&...feature-detect patterns (firebug-shim, ArrayBuffer detect, JSDOM detect); evaluate false in SW context at runtime; pre-existing; not introduced by this fix. - Manifest validation: unchanged — same permissions, same service_worker entry, same web_accessible_resources.
The diff is purely subtractive in the SW source (8 lines removed, 14 lines of code-comment + 1 new logger.log line added; no new imports, no new APIs, no new vendor dependencies). It cannot have introduced any of the pre-existing vendor matches.
Why this was latent until 2026-05-20
- D-01 (
chrome.tabCapture→getDisplayMediain offscreen) shipped in Plan 01-09; the tab dependency was orphaned at that point. - Plan 01-09 smoke validation only exercised the toolbar
action.onClickedpath, which grants activeTab and made the dead code transparent. - The original
notifStartupCTA copy wording was conservative ("Mokosh is ready"), not action-inviting; operators tended to dismiss the notification rather than click it. - Commit
4bba679(2026-05-20) splitnotifStartupintonotifStartupCta("Mokosh ready. Click to start a recording.")notifRecordingStarted. The new CTA copy explicitly invites the click — operators began exercising the path on browser restarts. The latent bug surfaced immediately.
Files modified
src/background/index.ts— startVideoCapture: removed lines 514-521 dead tab query + validation + tab-dependent log; replaced with a multi-line WHY comment + a tab-independentlogger.log('Starting video capture (whole-desktop via getDisplayMedia in offscreen per D-01)').tests/background/start-video-capture-no-tab.test.ts(NEW) — 3 tests pinning the new contract.
Forward-looking notes
captureScreenshot()andsaveArchive()still query the active tab; this is correct because they are reached only through gestures that grant activeTab (toolbar → popup → SAVE button). Do NOT generalise this fix to those functions — they have real downstream consumers of tab fields.- The "add
tabspermission to manifest" question is a separate scope-expansion decision (Phase 5 hardening candidate). It would let notifications.onClicked + future no-gesture paths read tab metadata, but it widens the permission surface. Not required for the current bug because the start-recording path doesn't need tab data. - Defensive UX feedback (e.g., showing an error notification when startVideoCapture fails) is out of scope here. After this fix the failure mode that motivated such UX is gone; if a future failure path emerges, address it then.
Operator UAT closure
Orchestrator re-spawns the operator UAT post-merge to confirm:
- Install built extension to fresh Chrome profile.
- Restart Chrome →
onStartupfires → mokosh-startup-* notification appears with CTA "Mokosh ready. Click to start a recording." - Click the notification → badge transitions to REC → Chrome shows
"Sharing your screen" banner → SW devtools console shows
Starting video capture (whole-desktop via getDisplayMedia in offscreen per D-01)+START_RECORDING sent successfully(NO "No active tab found" anywhere). - Toolbar click while not recording still starts a new session (regression guard against over-trimming).