Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
Showing only changes of commit ffd383d2a6 - Show all commits

View File

@@ -16,6 +16,20 @@ const VIDEO_MIME_FALLBACK = 'video/webm;codecs=vp9';
const logger = new Logger('Main');
// Option C: a typed error so saveArchive can distinguish the empty-video
// failure (operator-facing — shows a clear RECORDING_ERROR popup) from
// generic createArchive failures (zip/manifest/etc.). The 'empty-video-
// buffer' code joins the CaptureErrorCode union surfaced by the
// offscreen recorder (see src/offscreen/recorder.ts: classifyCaptureError)
// — same operator-facing vocabulary, different production point.
export class EmptyVideoBufferError extends Error {
readonly code = 'empty-video-buffer' as const;
constructor(detail: string) {
super(`empty-video-buffer: ${detail}`);
this.name = 'EmptyVideoBufferError';
}
}
// Состояние
// Видеобуфер живёт в offscreen-документе (D-16). SW не хранит чанки локально:
// при экспорте он спрашивает буфер у offscreen через long-lived port (D-17).
@@ -418,14 +432,28 @@ async function createArchive(
const zip = new JSZip();
// Добавляем видео (D-13: каждая запись — самодостаточный WebM-сегмент)
if (videoBufferResponse.segments.length > 0) {
// Добавляем видео (D-13: каждая запись — самодостаточный WebM-сегмент).
//
// Option C (debug session empty-archive-port-race): the upstream
// silent-skip branch is GONE. Shipping a zip with no video defeats the
// entire purpose of the operator-save flow (Phase 1 goal); the operator
// must see a clear failure instead of receiving a 88 KB archive with no
// last_30sec.webm. saveArchive's catch translates the throw into the
// {success: false, error: 'empty-video-buffer'} response shape the
// popup already handles via the RECORDING_ERROR surface.
if (videoBufferResponse.segments.length === 0) {
throw new EmptyVideoBufferError(
'no video segments available — buffer fetch returned empty (port replacement timed out, or recorder never started)',
);
}
const videoBlob = mergeVideoSegments(videoBufferResponse.segments);
if (videoBlob.size === 0) {
throw new EmptyVideoBufferError(
`merged video blob is zero bytes (segment count=${videoBufferResponse.segments.length})`,
);
}
zip.file('video/last_30sec.webm', videoBlob);
logger.log(`✓ Added video: ${videoBlob.size} bytes`);
} else {
logger.warn('✗ No video segments to add');
}
// Добавляем rrweb события
const rrwebJson = JSON.stringify(rrwebEvents, null, 2);
@@ -573,6 +601,23 @@ async function saveArchive() {
} catch (error) {
logger.error('Failed to save archive:', error);
// Option C: the empty-video failure is operator-visible. Emit
// RECORDING_ERROR so the popup's existing handler can surface it
// (same channel codec-unsupported, user-cancelled, etc. ride).
// Other createArchive failures (zip libs, JSZip internals) stay
// SW-side only — they're not actionable by the operator.
if (error instanceof EmptyVideoBufferError) {
try {
chrome.runtime.sendMessage({
type: 'RECORDING_ERROR',
error: error.code,
});
} catch (sendErr) {
// Best-effort notification — if the popup is closed we have
// nothing else to do.
logger.warn('Failed to broadcast RECORDING_ERROR:', sendErr);
}
}
return { success: false, error };
}
}