feat: add official Codex skill and harden task execution
Install and update artemis-use with task diagnostics and plugin guidance. Preserve cross-device file receipts, review canonical reports, guard stage transitions and reduce small history summaries. Improve exploration lifecycle, request recovery and browser live connections.
This commit is contained in:
1 parent
2328a4df0c
commit
e1ea099592
128 files changed
+4944
-700
No files matched your search
@@ -16,7 +16,9 @@
|
||||
|
||||
import { Injectable, signal, inject, computed, DestroyRef, NgZone } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { Observable, finalize, tap, timeout } from 'rxjs';
|
||||
import { UI_READ_TIMEOUT_MS } from '../core/http-timeouts';
|
||||
import { SharedLiveStream } from './shared-live-stream';
|
||||
|
||||
import { Session, ModelInfo, TaskQueueItem, AgentStatusResponse, SessionUsage, CaseReviewResponse } from '../core/models/session.model';
|
||||
import { ProTuningDefaults, ProTuningOptions } from '../core/models/pro-tuning.model';
|
||||
@@ -452,7 +454,12 @@ export class AgentService {
|
||||
: status;
|
||||
});
|
||||
|
||||
private eventSource: EventSource | null = null;
|
||||
private eventSource: SharedLiveStream | null = null;
|
||||
private destroyed = false;
|
||||
private pageHidden = false;
|
||||
private statusRequestPending = false;
|
||||
private sessionsRequestPending = false;
|
||||
private guidanceRequestsPending = new Set<string>();
|
||||
private statusInterval: any = null;
|
||||
private sessionLoadGeneration = 0;
|
||||
private sessionSnapshotRequestId = 0;
|
||||
@@ -467,11 +474,22 @@ export class AgentService {
|
||||
private lastActiveTasksSignature: string | null = null;
|
||||
private lastPersistedSessionsJson: string | null = null;
|
||||
private onVisibilityChange = () => {
|
||||
if (typeof document !== 'undefined' && !document.hidden) {
|
||||
if (document.hidden || this.pageHidden) {
|
||||
this.closeLiveStream();
|
||||
} else {
|
||||
this.fetchStatus();
|
||||
this.fetchSessions();
|
||||
this.ensureLiveStream();
|
||||
}
|
||||
};
|
||||
private onPageHide = () => {
|
||||
this.pageHidden = true;
|
||||
this.closeLiveStream();
|
||||
};
|
||||
private onPageShow = () => {
|
||||
this.pageHidden = false;
|
||||
this.onVisibilityChange();
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.restoreSessionsCache();
|
||||
@@ -480,6 +498,8 @@ export class AgentService {
|
||||
this.ensureLiveStream();
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('visibilitychange', this.onVisibilityChange);
|
||||
window.addEventListener('pagehide', this.onPageHide);
|
||||
window.addEventListener('pageshow', this.onPageShow);
|
||||
}
|
||||
inject(DestroyRef).onDestroy(() => this.destroy());
|
||||
}
|
||||
@@ -700,7 +720,12 @@ export class AgentService {
|
||||
* Fetch all past and active sessions from the backend
|
||||
*/
|
||||
public fetchSessions(): void {
|
||||
this.http.get<Session[]>('/api/sessions').subscribe({
|
||||
if (this.destroyed || this.sessionsRequestPending) return;
|
||||
this.sessionsRequestPending = true;
|
||||
this.http.get<Session[]>('/api/sessions').pipe(
|
||||
timeout(UI_READ_TIMEOUT_MS),
|
||||
finalize(() => this.sessionsRequestPending = false)
|
||||
).subscribe({
|
||||
next: (data) => {
|
||||
this.rawSessions.set(data);
|
||||
this.persistSessionsCache(data);
|
||||
@@ -850,26 +875,43 @@ export class AgentService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure persistent single-channel live stream is established via /api/stream.
|
||||
* Eliminates reconnect tearing, race conditions, and disconnect dropouts.
|
||||
* Keep a live connection only while this page can display updates. Hidden
|
||||
* tabs otherwise exhaust HTTP/1.1's per-origin connection pool, blocking
|
||||
* configuration and history requests in every tab of the same browser.
|
||||
*/
|
||||
public ensureLiveStream(): void {
|
||||
if (this.eventSource) {
|
||||
if (this.destroyed || this.pageHidden || document.hidden || this.eventSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Establishing persistent unified live stream via /api/stream');
|
||||
const eventTypes = [
|
||||
'llm_stream', 'llm_stream_reset', 'trace_recorded', 'step_recorded',
|
||||
'step_updated', 'background_tasks_updated', 'task_paused', 'task_resumed',
|
||||
'llm_retrying', 'startup_progress', 'session_started', 'session_ended',
|
||||
'recording_ready', 'recording_failed', 'checker_event'
|
||||
];
|
||||
// The stream is registered outside the Angular zone: high-frequency SSE
|
||||
// callbacks must not schedule a change-detection pass each. Signal writes
|
||||
// still notify the render scheduler, so the UI stays live.
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.eventSource = new EventSource('/api/stream');
|
||||
this.eventSource = SharedLiveStream.open('/api/stream', ['info', 'keep-alive', ...eventTypes]);
|
||||
const source = this.eventSource;
|
||||
|
||||
this.eventSource.addEventListener('info', () => {
|
||||
// Reconcile current session if active
|
||||
if (this.eventSource !== source) return;
|
||||
// Events may have been missed while hidden or during a server restart.
|
||||
this.fetchStatus();
|
||||
this.fetchSessions();
|
||||
const curId = this.currentSessionId();
|
||||
if (curId) {
|
||||
this.backfillSessionSteps(curId);
|
||||
this.fetchChecks(curId);
|
||||
this.fetchNotes(curId);
|
||||
this.fetchGuidance(curId);
|
||||
}
|
||||
if (this.isVideoWindowOpen() && ['live', 'processing'].includes(this.recordingPlaybackStatus())) {
|
||||
this.refreshActiveRecording(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -877,26 +919,9 @@ export class AgentService {
|
||||
// Keep-alive tick
|
||||
});
|
||||
|
||||
const eventTypes = [
|
||||
'llm_stream',
|
||||
'llm_stream_reset',
|
||||
'trace_recorded',
|
||||
'step_recorded',
|
||||
'step_updated',
|
||||
'background_tasks_updated',
|
||||
'task_paused',
|
||||
'task_resumed',
|
||||
'llm_retrying',
|
||||
'startup_progress',
|
||||
'session_started',
|
||||
'session_ended',
|
||||
'recording_ready',
|
||||
'recording_failed',
|
||||
'checker_event'
|
||||
];
|
||||
|
||||
eventTypes.forEach((eventType) => {
|
||||
this.eventSource?.addEventListener(eventType, (event: MessageEvent) => {
|
||||
this.eventSource?.addEventListener(eventType, ((event: MessageEvent) => {
|
||||
if (this.eventSource !== source) return;
|
||||
try {
|
||||
const parsedData = JSON.parse(event.data);
|
||||
const evtSessionId = parsedData?.session_id;
|
||||
@@ -1197,7 +1222,7 @@ export class AgentService {
|
||||
}
|
||||
]);
|
||||
}
|
||||
});
|
||||
}) as EventListener);
|
||||
});
|
||||
|
||||
this.eventSource.onerror = (err) => {
|
||||
@@ -1206,6 +1231,12 @@ export class AgentService {
|
||||
});
|
||||
}
|
||||
|
||||
private closeLiveStream(): void {
|
||||
this.eventSource?.close();
|
||||
this.eventSource = null;
|
||||
this.flushStreamChunks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer one llm_stream chunk; the buffer is flushed into sessionLogs at most
|
||||
* once per short window (and immediately before any non-stream event so the
|
||||
@@ -1375,8 +1406,12 @@ export class AgentService {
|
||||
}
|
||||
|
||||
public fetchGuidance(sessionId: string): void {
|
||||
if (this.currentSessionId() !== sessionId) return;
|
||||
this.http.get<GuidanceEvent[]>(`/api/tasks/${encodeURIComponent(sessionId)}/guidance`).subscribe({
|
||||
if (this.destroyed || this.currentSessionId() !== sessionId || this.guidanceRequestsPending.has(sessionId)) return;
|
||||
this.guidanceRequestsPending.add(sessionId);
|
||||
this.http.get<GuidanceEvent[]>(`/api/tasks/${encodeURIComponent(sessionId)}/guidance`).pipe(
|
||||
timeout(UI_READ_TIMEOUT_MS),
|
||||
finalize(() => this.guidanceRequestsPending.delete(sessionId))
|
||||
).subscribe({
|
||||
next: (items) => {
|
||||
if (this.currentSessionId() !== sessionId) return;
|
||||
const signature = JSON.stringify(items);
|
||||
@@ -1618,7 +1653,12 @@ export class AgentService {
|
||||
* Fetch current agent runner process status
|
||||
*/
|
||||
public fetchStatus(): void {
|
||||
this.http.get<any>('/api/status').subscribe({
|
||||
if (this.destroyed || this.statusRequestPending) return;
|
||||
this.statusRequestPending = true;
|
||||
this.http.get<any>('/api/status').pipe(
|
||||
timeout(UI_READ_TIMEOUT_MS),
|
||||
finalize(() => this.statusRequestPending = false)
|
||||
).subscribe({
|
||||
next: (data) => {
|
||||
if (data && data.status) {
|
||||
const oldStatus = this.agentStatus();
|
||||
@@ -1718,6 +1758,7 @@ export class AgentService {
|
||||
* Clean up event source connection and timers on service destruction
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.destroyed = true;
|
||||
if (this.statusInterval) {
|
||||
clearInterval(this.statusInterval);
|
||||
this.statusInterval = null;
|
||||
@@ -1729,6 +1770,8 @@ export class AgentService {
|
||||
this.discardPendingStreamChunks();
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('visibilitychange', this.onVisibilityChange);
|
||||
window.removeEventListener('pagehide', this.onPageHide);
|
||||
window.removeEventListener('pageshow', this.onPageShow);
|
||||
}
|
||||
this.cancelVideoRetry();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user