mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-12 18:02:20 +02:00
Squash Odysseus development history
This commit is contained in:
+129
-7
@@ -10,6 +10,7 @@ let _idCounter = 0;
|
||||
// Dismissed-from-panel IDs persist across reloads so Clear actually sticks.
|
||||
// (Items still live on disk and in the Library; this just hides them here.)
|
||||
const _DISMISSED_KEY = 'odysseus-research-dismissed';
|
||||
const _ERRORS_KEY = 'odysseus-research-errors';
|
||||
function _loadDismissed() {
|
||||
try {
|
||||
const raw = localStorage.getItem(_DISMISSED_KEY);
|
||||
@@ -26,17 +27,60 @@ function _markDismissed(ids) {
|
||||
_saveDismissed(set);
|
||||
}
|
||||
|
||||
function _loadSavedErrors() {
|
||||
try {
|
||||
const raw = localStorage.getItem(_ERRORS_KEY);
|
||||
const items = raw ? JSON.parse(raw) : [];
|
||||
return Array.isArray(items) ? items : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
function _saveErrors(items) {
|
||||
try { localStorage.setItem(_ERRORS_KEY, JSON.stringify(items.slice(-20))); } catch {}
|
||||
}
|
||||
function _rememberError(job) {
|
||||
const items = _loadSavedErrors().filter(item => item.id !== job.id);
|
||||
items.push({
|
||||
id: job.id,
|
||||
query: job.query,
|
||||
status: 'error',
|
||||
errorMsg: job.errorMsg || 'Research failed.',
|
||||
startedAt: job.startedAt || Date.now(),
|
||||
elapsed: job.elapsed || 0,
|
||||
category: job.category || '',
|
||||
mode: job.mode || 'research',
|
||||
modelName: job.modelName || null,
|
||||
settings: job.settings || {},
|
||||
});
|
||||
_saveErrors(items);
|
||||
}
|
||||
function _forgetError(id) {
|
||||
_saveErrors(_loadSavedErrors().filter(item => item.id !== id));
|
||||
}
|
||||
|
||||
let _activePollInterval = null;
|
||||
let _activePollInFlight = false;
|
||||
let _librarySyncInFlight = false;
|
||||
let _lastLibrarySyncAt = 0;
|
||||
const _LIBRARY_SYNC_MIN_MS = 120000;
|
||||
|
||||
export function init(apiBase) {
|
||||
_apiBase = apiBase;
|
||||
_reconnectActive();
|
||||
// Restore failed launches as history so a refresh does not erase the only
|
||||
// explanation the user received for a failed research request.
|
||||
_jobs = _loadSavedErrors().map(item => ({
|
||||
...item,
|
||||
progress: {}, result: null, sources: null, findings: null,
|
||||
errorMsg: item.errorMsg || 'Research failed.',
|
||||
avgDuration: null, endpointName: null,
|
||||
_es: null, _timerInterval: null,
|
||||
}));
|
||||
_reconnectActive({ includeLibrary: true, forceLibrary: true });
|
||||
// Poll for active sessions periodically so research started elsewhere
|
||||
// (e.g. by the agent via trigger_research) gets adopted into the
|
||||
// sidebar — _reconnectActive only ran once at load before, so
|
||||
// agent-started jobs never appeared until a page reload.
|
||||
if (_activePollInterval) clearInterval(_activePollInterval);
|
||||
_activePollInterval = setInterval(() => { _reconnectActive(); }, 12000);
|
||||
_activePollInterval = setInterval(() => { _reconnectActive(); }, 20000);
|
||||
}
|
||||
|
||||
// Allow an immediate adopt when the chat stream signals a new research
|
||||
@@ -46,7 +90,13 @@ export function adoptSession(sessionId) {
|
||||
_reconnectActive();
|
||||
}
|
||||
|
||||
async function _reconnectActive() {
|
||||
export function refreshLibrary(options = {}) {
|
||||
return _syncLibrary(options);
|
||||
}
|
||||
|
||||
async function _reconnectActive(options = {}) {
|
||||
if (_activePollInFlight) return;
|
||||
_activePollInFlight = true;
|
||||
try {
|
||||
// Reconnect to running tasks
|
||||
const res = await fetch(`${_apiBase}/api/research/active`, { credentials: 'same-origin' });
|
||||
@@ -60,7 +110,13 @@ async function _reconnectActive() {
|
||||
startedAt: task.started_at ? task.started_at * 1000 : Date.now(),
|
||||
elapsed: task.started_at ? Date.now() - task.started_at * 1000 : 0,
|
||||
result: null, sources: null, findings: null,
|
||||
source_state: task.source_state || '',
|
||||
source_coverage: task.source_coverage || {},
|
||||
navigation_trace: Array.isArray(task.navigation_trace) ? task.navigation_trace : [],
|
||||
action_trace: Array.isArray(task.action_trace) ? task.action_trace : [],
|
||||
errorMsg: null, avgDuration: null, modelName: null,
|
||||
category: task.category || '',
|
||||
mode: task.mode || 'research',
|
||||
settings: {}, _es: null, _timerInterval: null,
|
||||
};
|
||||
_jobs.push(job);
|
||||
@@ -68,7 +124,20 @@ async function _reconnectActive() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load recent completed research from disk
|
||||
if (options.includeLibrary) await _syncLibrary({ force: !!options.forceLibrary });
|
||||
_notify();
|
||||
} catch {
|
||||
} finally {
|
||||
_activePollInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function _syncLibrary(options = {}) {
|
||||
const now = Date.now();
|
||||
if (_librarySyncInFlight) return;
|
||||
if (!options.force && now - _lastLibrarySyncAt < _LIBRARY_SYNC_MIN_MS) return;
|
||||
_librarySyncInFlight = true;
|
||||
try {
|
||||
const libRes = await fetch(`${_apiBase}/api/research/library?sort=recent&limit=20`, { credentials: 'same-origin' });
|
||||
if (libRes.ok) {
|
||||
const libData = await libRes.json();
|
||||
@@ -76,23 +145,49 @@ async function _reconnectActive() {
|
||||
for (const item of (libData.research || [])) {
|
||||
if (item.status !== 'done') continue;
|
||||
if (dismissed.has(item.id)) continue;
|
||||
if (_jobs.some(j => j.id === item.id)) continue;
|
||||
const elapsed = item.duration ? _parseDuration(item.duration) : 0;
|
||||
const existing = _jobs.find(j => j.id === item.id);
|
||||
if (existing) {
|
||||
let changed = false;
|
||||
const updates = {
|
||||
query: item.query || existing.query,
|
||||
status: 'done',
|
||||
elapsed: elapsed || existing.elapsed || 0,
|
||||
sourceCount: item.source_count || existing.sourceCount || 0,
|
||||
thumbnail: item.thumbnail || existing.thumbnail || '',
|
||||
category: item.category || existing.category || '',
|
||||
mode: item.mode || existing.mode || 'research',
|
||||
_fromLibrary: true,
|
||||
};
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (existing[key] !== value) {
|
||||
existing[key] = value;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) _notify();
|
||||
continue;
|
||||
}
|
||||
_jobs.push({
|
||||
id: item.id, query: item.query, status: 'done',
|
||||
progress: {}, startedAt: (item.started_at || 0) * 1000,
|
||||
elapsed, result: null, sources: null, findings: null,
|
||||
sourceCount: item.source_count || 0,
|
||||
thumbnail: item.thumbnail || '',
|
||||
category: item.category || '',
|
||||
mode: item.mode || 'research',
|
||||
errorMsg: null, avgDuration: null, modelName: null,
|
||||
settings: { max_rounds: item.rounds || 8 },
|
||||
_es: null, _timerInterval: null, _fromLibrary: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_lastLibrarySyncAt = Date.now();
|
||||
_notify();
|
||||
} catch {}
|
||||
finally {
|
||||
_librarySyncInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function _parseDuration(s) {
|
||||
@@ -145,6 +240,7 @@ export async function startAllQueuedSequential() {
|
||||
export async function retryJob(jobId) {
|
||||
const job = _jobs.find(j => j.id === jobId);
|
||||
if (!job) return;
|
||||
_forgetError(jobId);
|
||||
job.status = 'queued';
|
||||
job.progress = {};
|
||||
job.errorMsg = null;
|
||||
@@ -171,6 +267,7 @@ export function removeJob(id) {
|
||||
const job = _jobs[idx];
|
||||
// Persist dismissal so it doesn't reappear from the library on reload.
|
||||
if (job.status === 'done') _markDismissed([id]);
|
||||
if (job.status === 'error') _forgetError(id);
|
||||
_jobs.splice(idx, 1);
|
||||
}
|
||||
_notify();
|
||||
@@ -180,6 +277,7 @@ export function clearAll() {
|
||||
// Mark all completed jobs as dismissed so they don't reappear on reload.
|
||||
const doneIds = _jobs.filter(j => j.status === 'done').map(j => j.id);
|
||||
if (doneIds.length) _markDismissed(doneIds);
|
||||
_saveErrors([]);
|
||||
for (const job of _jobs) {
|
||||
if (job._es) { job._es.close(); job._es = null; }
|
||||
if (job._timerInterval) { clearInterval(job._timerInterval); job._timerInterval = null; }
|
||||
@@ -216,6 +314,7 @@ function _makeJob(query, settings) {
|
||||
progress: {}, startedAt: null, elapsed: 0,
|
||||
result: null, sources: null, findings: null,
|
||||
category: settings?.category || '',
|
||||
mode: 'research',
|
||||
errorMsg: null, avgDuration: null,
|
||||
modelName: null, endpointName: null,
|
||||
_es: null, _timerInterval: null,
|
||||
@@ -235,6 +334,7 @@ async function _launchJob(job) {
|
||||
const txt = await res.text();
|
||||
try { job.errorMsg = JSON.parse(txt).detail || txt; } catch { job.errorMsg = txt; }
|
||||
job.status = 'error';
|
||||
_rememberError(job);
|
||||
_notify();
|
||||
return;
|
||||
}
|
||||
@@ -242,10 +342,13 @@ async function _launchJob(job) {
|
||||
} catch (e) {
|
||||
job.errorMsg = e.message;
|
||||
job.status = 'error';
|
||||
_rememberError(job);
|
||||
_notify();
|
||||
return;
|
||||
}
|
||||
job.id = data.session_id;
|
||||
if (data.category) job.category = data.category;
|
||||
if (data.mode) job.mode = data.mode;
|
||||
job.status = 'running';
|
||||
job.startedAt = Date.now();
|
||||
_connectStream(job);
|
||||
@@ -266,6 +369,12 @@ function _connectStream(job) {
|
||||
const d = JSON.parse(evt.data);
|
||||
if (d.status === 'not_found') { _finishJob(job, 'error'); return; }
|
||||
job.progress = d;
|
||||
if (d.source_state) job.source_state = d.source_state;
|
||||
if (d.source_coverage) job.source_coverage = d.source_coverage;
|
||||
if (Array.isArray(d.navigation_trace)) job.navigation_trace = d.navigation_trace;
|
||||
if (Array.isArray(d.action_trace)) job.action_trace = d.action_trace;
|
||||
if (d.category) job.category = d.category;
|
||||
if (d.mode) job.mode = d.mode;
|
||||
if (d.model && !job.modelName) job.modelName = d.model;
|
||||
if (d.final) {
|
||||
if (d.error) job.errorMsg = d.error;
|
||||
@@ -290,6 +399,12 @@ async function _pollFallback(job) {
|
||||
if (!res.ok) { _finishJob(job, 'error'); return; }
|
||||
const d = await res.json();
|
||||
job.progress = d.progress || {};
|
||||
if (d.source_state) job.source_state = d.source_state;
|
||||
if (d.source_coverage) job.source_coverage = d.source_coverage;
|
||||
if (Array.isArray(d.navigation_trace)) job.navigation_trace = d.navigation_trace;
|
||||
if (Array.isArray(d.action_trace)) job.action_trace = d.action_trace;
|
||||
if (d.category) job.category = d.category;
|
||||
if (d.mode) job.mode = d.mode;
|
||||
if (d.avg_duration) job.avgDuration = d.avg_duration;
|
||||
if (d.status !== 'running') {
|
||||
_finishJob(job, d.status === 'done' ? 'done' : 'error');
|
||||
@@ -302,6 +417,7 @@ async function _pollFallback(job) {
|
||||
|
||||
function _finishJob(job, status) {
|
||||
job.status = status;
|
||||
if (status === 'error') _rememberError(job);
|
||||
if (job._es) { job._es.close(); job._es = null; }
|
||||
if (job._timerInterval) { clearInterval(job._timerInterval); job._timerInterval = null; }
|
||||
job.elapsed = Date.now() - (job.startedAt || Date.now());
|
||||
@@ -327,7 +443,13 @@ async function _fetchResult(job) {
|
||||
job.result = d.result;
|
||||
job.sources = d.sources;
|
||||
job.findings = d.raw_findings;
|
||||
if (d.category && !job.category) job.category = d.category;
|
||||
job.analyzed_urls = d.analyzed_urls;
|
||||
job.source_state = d.source_state;
|
||||
job.source_coverage = d.source_coverage || {};
|
||||
job.navigation_trace = Array.isArray(d.navigation_trace) ? d.navigation_trace : [];
|
||||
job.action_trace = Array.isArray(d.action_trace) ? d.action_trace : [];
|
||||
if (d.category) job.category = d.category;
|
||||
if (d.mode) job.mode = d.mode;
|
||||
_notify();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
+884
-309
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user