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:
chenmingxuan committed 2026-10-18 14:18:37 +08:00
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();
}