fix: finalize recordings before automatic replay

This commit is contained in:
somew1nd committed 2026-08-23 13:06:19 -07:00
1 parent 474e56ef8e
commit a62ac4aef1
25 files changed
+820 -92

No files matched your search

@@ -30,6 +30,24 @@ export interface VideoSegment {
height: number;
}
export type RecordingPlaybackStatus =
| 'idle'
| 'live'
| 'processing'
| 'ready'
| 'failed'
| 'unavailable';
interface SessionVideoResponse {
session_id: string;
status?: 'processing' | 'ready' | 'failed' | 'unavailable';
has_video: boolean;
video_url: string | null;
video_segments?: VideoSegment[];
retry_after_ms?: number;
message?: string;
}
@Injectable({
providedIn: 'root'
})
@@ -115,6 +133,13 @@ export class AgentService {
public activeVideoSegments = signal<VideoSegment[]>([]);
public activeVideoTitle = signal<string>('');
public isVideoLoading = signal<boolean>(false);
public recordingPlaybackStatus = signal<RecordingPlaybackStatus>('idle');
public recordingPlaybackMessage = signal<string>('');
public shouldAutoplayVideo = signal<boolean>(false);
private activeVideoSessionId: string | null = null;
private videoRequestGeneration = 0;
private videoRetryTimer: ReturnType<typeof setTimeout> | null = null;
private videoWaitStartedAt = 0;
/**
* Clear user-pinned selection so subsequent runs automatically follow active runner
@@ -151,6 +176,14 @@ export class AgentService {
return session?.video_url || null;
});
public currentSessionRecordingStatus = computed(() => {
const session = this.currentSession();
const status = session?.recording_status;
return status === 'recording' || status === 'finalizing' || status === 'processing'
? 'processing'
: status;
});
private eventSource: EventSource | null = null;
private statusInterval: any = null;
@@ -441,7 +474,9 @@ export class AgentService {
'task_resumed',
'llm_retrying',
'session_started',
'session_ended'
'session_ended',
'recording_ready',
'recording_failed'
];
eventTypes.forEach((eventType) => {
@@ -451,6 +486,29 @@ export class AgentService {
// If the event carries a session_id for a different session, update global states if applicable and ignore
const evtSessionId = parsedData?.session_id;
if (eventType === 'recording_ready' || eventType === 'recording_failed') {
if (
this.isVideoWindowOpen()
&& evtSessionId
&& String(evtSessionId) === String(this.activeVideoSessionId)
) {
if (eventType === 'recording_ready') {
this.refreshActiveRecording(true);
} else {
this.cancelVideoRetry();
this.isVideoLoading.set(false);
this.activeVideoUrl.set(null);
this.activeVideoSegments.set([]);
this.recordingPlaybackStatus.set('failed');
this.recordingPlaybackMessage.set(
parsedData?.error || 'Recording finalization failed.'
);
}
}
return;
}
if (evtSessionId && String(evtSessionId) !== String(sessionId)) {
if (eventType === 'session_started') {
this.agentStatus.set('running');
@@ -498,6 +556,13 @@ export class AgentService {
this.fetchSessions();
this.fetchNotes(sessionId);
this.updateModelForCurrentView();
if (
this.isVideoWindowOpen()
&& this.activeVideoSessionId === sessionId
&& this.recordingPlaybackStatus() === 'live'
) {
this.beginRecordingFinalization(sessionId);
}
return;
}
@@ -912,6 +977,17 @@ export class AgentService {
this.fetchSessions();
}
if (
!isActive
&& (oldStatus === 'running' || oldStatus === 'paused')
&& oldRunningSessionId
&& this.isVideoWindowOpen()
&& this.activeVideoSessionId === oldRunningSessionId
&& this.recordingPlaybackStatus() === 'live'
) {
this.beginRecordingFinalization(oldRunningSessionId);
}
// Auto-select the session if running and user hasn't explicitly chosen to inspect another task
if (isActive && data.session_id) {
const currentId = this.currentSessionId();
@@ -961,6 +1037,7 @@ export class AgentService {
if (this.eventSource) {
this.eventSource.close();
}
this.cancelVideoRetry();
}
/**
@@ -1008,35 +1085,147 @@ export class AgentService {
this.activeVideoTitle.set(goalTitle);
this.isVideoWindowOpen.set(true);
this.isVideoMinimized.set(false);
this.activeVideoSessionId = targetSessionId;
this.cancelVideoRetry();
this.videoRequestGeneration++;
this.shouldAutoplayVideo.set(false);
this.recordingPlaybackMessage.set('');
this.activeVideoSegments.set([]);
if (!targetSessionId) {
this.activeVideoUrl.set(null);
this.isVideoLoading.set(false);
this.recordingPlaybackStatus.set('unavailable');
return;
}
if (session?.status === 'running' || session?.status === 'paused') {
this.activeVideoUrl.set(null);
this.isVideoLoading.set(false);
this.recordingPlaybackStatus.set('live');
return;
}
this.activeVideoUrl.set(targetUrl);
this.shouldAutoplayVideo.set(true);
this.videoWaitStartedAt = Date.now();
this.isVideoLoading.set(true);
this.recordingPlaybackStatus.set('processing');
this.recordingPlaybackMessage.set('Loading screen recording...');
this.requestSessionVideo(targetSessionId, this.videoRequestGeneration);
}
private beginRecordingFinalization(sessionId: string): void {
if (this.activeVideoSessionId !== sessionId) return;
this.cancelVideoRetry();
this.videoRequestGeneration++;
this.videoWaitStartedAt = Date.now();
this.activeVideoUrl.set(null);
this.activeVideoSegments.set([]);
if (targetSessionId) {
this.isVideoLoading.set(true);
this.http.get<{ session_id: string; has_video: boolean; video_url: string | null; video_segments?: VideoSegment[] }>(`/api/sessions/${targetSessionId}/video`).subscribe({
next: (res) => {
this.isVideoLoading.set(false);
if (res && res.has_video && res.video_url) {
this.activeVideoUrl.set(res.video_url);
this.activeVideoSegments.set(res.video_segments || []);
this.rawSessions.update((list) =>
list.map((s) => s.session_id === targetSessionId ? { ...s, video_url: res.video_url || undefined } : s)
);
} else {
this.activeVideoUrl.set(null);
this.activeVideoSegments.set([]);
}
},
error: () => {
this.shouldAutoplayVideo.set(true);
this.isVideoLoading.set(true);
this.recordingPlaybackStatus.set('processing');
this.recordingPlaybackMessage.set('Finalizing screen recording...');
this.requestSessionVideo(sessionId, this.videoRequestGeneration);
}
private requestSessionVideo(sessionId: string, generation: number): void {
this.http.get<SessionVideoResponse>(`/api/sessions/${sessionId}/video`).subscribe({
next: (res) => {
if (generation !== this.videoRequestGeneration || sessionId !== this.activeVideoSessionId) {
return;
}
const status = res.status || (res.has_video && res.video_url ? 'ready' : 'unavailable');
if (status === 'ready' && res.video_url) {
this.cancelVideoRetry();
this.isVideoLoading.set(false);
this.recordingPlaybackStatus.set('ready');
this.recordingPlaybackMessage.set('');
this.activeVideoUrl.set(res.video_url);
this.activeVideoSegments.set(res.video_segments || []);
this.rawSessions.update((list) =>
list.map((s) => s.session_id === sessionId
? { ...s, video_url: res.video_url || undefined, recording_status: 'ready' }
: s)
);
return;
}
if (status === 'processing') {
this.activeVideoUrl.set(null);
this.activeVideoSegments.set([]);
this.isVideoLoading.set(true);
this.recordingPlaybackStatus.set('processing');
this.recordingPlaybackMessage.set('Finalizing screen recording...');
this.scheduleVideoRetry(sessionId, generation, res.retry_after_ms);
return;
}
});
} else {
this.activeVideoUrl.set(null);
this.activeVideoSegments.set([]);
this.cancelVideoRetry();
this.isVideoLoading.set(false);
this.activeVideoUrl.set(null);
this.activeVideoSegments.set([]);
this.recordingPlaybackStatus.set(status === 'failed' ? 'failed' : 'unavailable');
this.recordingPlaybackMessage.set(
res.message || (status === 'failed'
? 'Recording finalization failed.'
: 'No screen recording is available for this task.')
);
},
error: () => {
if (generation !== this.videoRequestGeneration || sessionId !== this.activeVideoSessionId) {
return;
}
if (this.recordingPlaybackStatus() === 'processing') {
this.scheduleVideoRetry(sessionId, generation, 1000);
return;
}
this.isVideoLoading.set(false);
this.recordingPlaybackStatus.set('failed');
this.recordingPlaybackMessage.set('Unable to load the screen recording.');
}
});
}
private scheduleVideoRetry(sessionId: string, generation: number, retryAfterMs = 1000): void {
this.cancelVideoRetry();
if (Date.now() - this.videoWaitStartedAt > 120_000) {
this.isVideoLoading.set(false);
this.recordingPlaybackStatus.set('failed');
this.recordingPlaybackMessage.set('Recording finalization timed out. You can retry.');
return;
}
const delay = Math.max(500, Math.min(3000, retryAfterMs));
this.videoRetryTimer = setTimeout(() => {
this.videoRetryTimer = null;
this.requestSessionVideo(sessionId, generation);
}, delay);
}
private cancelVideoRetry(): void {
if (this.videoRetryTimer) {
clearTimeout(this.videoRetryTimer);
this.videoRetryTimer = null;
}
}
private refreshActiveRecording(autoplay: boolean): void {
const sessionId = this.activeVideoSessionId;
if (!sessionId) return;
this.cancelVideoRetry();
this.videoRequestGeneration++;
if (autoplay) this.shouldAutoplayVideo.set(true);
this.requestSessionVideo(sessionId, this.videoRequestGeneration);
}
public retryVideoRecording(): void {
const sessionId = this.activeVideoSessionId;
if (!sessionId) return;
this.beginRecordingFinalization(sessionId);
}
public consumeVideoAutoplay(): boolean {
const shouldAutoplay = this.shouldAutoplayVideo();
this.shouldAutoplayVideo.set(false);
return shouldAutoplay;
}
/**
@@ -1055,5 +1244,8 @@ export class AgentService {
*/
public closeVideoPlayer(): void {
this.isVideoWindowOpen.set(false);
this.cancelVideoRetry();
this.videoRequestGeneration++;
this.activeVideoSessionId = null;
}
}