Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent e5c99a5eee
commit 6ee6502010
2050 changed files with 538359 additions and 57745 deletions
+129 -7
View File
@@ -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 {}
}
File diff suppressed because it is too large Load Diff